Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ Returns a response object where:
- `req` - the simulated request object.
- `res` - the simulated response object.
- `headers` - an object containing the response headers.
- `statusCode` - the HTTP status code.
- `statusCode` - the HTTP status code. If response is aborted before headers are sent, the code is `499`.
- `statusMessage` - the HTTP status message.
- `payload` - the payload as a UTF-8 encoded string.
- `rawPayload` - the raw payload as a Buffer.
- `trailers` - an object containing the response trailers.
- `aborted` - optional property which is `true` for aborted, ie. not fully transmitted, responses.

### `Shot.isInjection(obj)`

Expand Down
7 changes: 6 additions & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface ResponseObject {
headers: OutgoingHttpHeaders;

/**
* The HTTP status code.
* The HTTP status code. If response is aborted before headers are sent, the code is `499`.
*/
statusCode: number;

Expand All @@ -70,6 +70,11 @@ export interface ResponseObject {
* An object containing the response trailers
*/
trailers: NodeJS.Dict<string>;

/**
* A boolean which is `true` for aborted, ie. not fully transmitted, responses.
*/
aborted?: true;
}

type PartialURL = Pick<UrlObject, 'protocol' | 'hostname' | 'port' | 'query'> & { pathname: string };
Expand Down
27 changes: 24 additions & 3 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
const Http = require('http');
const Stream = require('stream');

const Hoek = require('@hapi/hoek');

const Symbols = require('./symbols');


Expand All @@ -17,21 +19,40 @@ exports = module.exports = internals.Response = class extends Http.ServerRespons
this._shot = { headers: null, trailers: {}, payloadChunks: [] };
this.assignSocket(internals.nullSocket());

this.socket.on('error', Hoek.ignore); // The socket can be destroyed with an error

if (req._shot.simulate.close) {
// Ensure premature, manual close is forwarded to res.
// In HttpServer the socket closing actually triggers close on both req and res.
req.once('close', () => {

process.nextTick(() => this.emit('close'));
process.nextTick(() => this.destroy());
});
}

this.once('finish', () => {
const finalize = (aborted) => {

const res = internals.payload(this);
res.raw.req = req;
if (aborted) {
res.aborted = aborted;
if (!this.headersSent) {
res.statusCode = 499;
}
}

this.removeListener('close', abort);

process.nextTick(() => onEnd(res));
});
};

const abort = () => finalize(true);

this.once('finish', finalize);

// Add fallback listener that will not be called if 'finish' is emitted first

this.on('close', abort);
}

writeHead(...args) {
Expand Down
84 changes: 84 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,90 @@ describe('inject()', () => {
const err = await expect(Shot.inject((req, res) => { }, { url: '/', simulate: { end: 'wrong input' } })).to.reject();
expect(err.isJoi).to.be.true();
});

it('returns aborted on immediate res.destroy()', async () => {

const dispatch = function (req, res) {

res.destroy();
};

const res = await Shot.inject(dispatch, { method: 'get', url: '/' });
expect(res.aborted).to.be.true();
expect(res.statusCode).to.equal(499);
});

it('returns aborted on immediate res.destroy(error)', async () => {

const dispatch = function (req, res) {

res.destroy(new Error('stop'));
};

const res = await Shot.inject(dispatch, { method: 'get', url: '/' });
expect(res.aborted).to.be.true();
expect(res.statusCode).to.equal(499);
});

it('returns aborted on res.destroy() while transmitting payload', async () => {

const dispatch = function (req, res) {

res.writeHead(404);
res.write('data');
setTimeout(() => res.destroy(), 1);
};

const res = await Shot.inject(dispatch, { method: 'get', url: '/' });
expect(res.aborted).to.be.true();
expect(res.statusCode).to.equal(404);
expect(res.payload).to.equal('data');
});

it('returns aborted on res.destroy(error) while transmitting payload', async () => {

const dispatch = function (req, res) {

res.writeHead(404);
res.write('data');
setTimeout(() => res.destroy(new Error('stop')), 1);
};

const res = await Shot.inject(dispatch, { method: 'get', url: '/' });
expect(res.aborted).to.be.true();
expect(res.statusCode).to.equal(404);
expect(res.payload).to.equal('data');
});

it('handles res.destroy() after transmitting payload', async () => {

const dispatch = function (req, res) {

res.writeHead(404);
res.end('data');
res.destroy();
};

const res = await Shot.inject(dispatch, { method: 'get', url: '/' });
expect(res.aborted).to.not.exist();
expect(res.statusCode).to.equal(404);
expect(res.payload).to.equal('data');
});

it('handles res.destroy(error) after transmitting payload', async () => {

const dispatch = function (req, res) {

res.writeHead(404);
res.end('data');
res.destroy(new Error('stop'));
};

const res = await Shot.inject(dispatch, { method: 'get', url: '/' });
expect(res.aborted).to.not.exist();
expect(res.statusCode).to.equal(404);
expect(res.payload).to.equal('data');
});
});

describe('writeHead()', () => {
Expand Down
Loading