Skip to content
Open
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
40 changes: 25 additions & 15 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,21 +448,22 @@ exports = module.exports = internals.Request = class {
this._setResponse(this._core.Response.wrap(exit, this)); // Wrap to ensure any object thrown is always a valid Boom or Response object
}

if (!this._eventContext.request) {
this._finalize();
return;
}
if (!this._eventContext.request || // Client disconnected
typeof this.response === 'symbol') { // close or abandon

if (typeof this.response === 'symbol') { // close or abandon
this._abort();
return;
}

await this._postCycle();

if (!this._eventContext.request ||
if (!this._eventContext.request || // Client disconnected during onPreResponse
typeof this.response === 'symbol') { // close or abandon

if (typeof this.response !== 'symbol') {
this._setResponse(internals.disconnectError(this));
}

this._abort();
return;
}
Expand Down Expand Up @@ -496,7 +497,11 @@ exports = module.exports = internals.Request = class {

_abort() {

if (this.response === Toolkit.symbols.close) {
if (this.response !== Toolkit.symbols.abandon) {
if (typeof this.response !== 'symbol') { // Client disconnected before transmission
this.raw.res.statusCode = this.response.output.statusCode;
}

this.raw.res.end(); // End the response in case it wasn't already closed
}

Expand Down Expand Up @@ -777,21 +782,26 @@ internals.event = function ({ request }, event, err) {

request._eventContext.request = null;

if (event === 'abort') {
// Request 'aborted' or response 'close' before the response was sent: the client disconnected.

// Calling _reply() means that the abort is applied immediately, unless the response has already
// called _reply(), in which case this call is ignored and the transmit logic is responsible for
// handling the abort.
// Calling _reply() means that the disconnect is applied immediately, unless the response has already
// called _reply(), in which case this call is ignored and the reply / transmit logic is responsible for
// handling the disconnect.

request._reply(new Boom.Boom('Request aborted', { statusCode: request.route.settings.response.disconnectStatusCode, data: request.response }));
request._reply(internals.disconnectError(request));

if (request._events) {
request._events.emit('disconnect');
}
if (request._events) {
request._events.emit('disconnect');
}
};


internals.disconnectError = function (request) {

return new Boom.Boom('Request aborted', { statusCode: request.route.settings.response.disconnectStatusCode, data: request.response });
};


internals.timeoutReply = function (request, timeout) {

const elapsed = Date.now() - request.info.received;
Expand Down
2 changes: 1 addition & 1 deletion test/payload.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('Payload', () => {
server.inject({ method: 'POST', url: '/', payload: 'test', simulate: { close: true, end: false } });
const request = await responded;
expect(request._isReplied).to.equal(true);
expect(request.response.output.statusCode).to.equal(500);
expect(request.response.output.statusCode).to.equal(499);
});

it('handles aborted request mid-lifecycle step', async (flags) => {
Expand Down
174 changes: 174 additions & 0 deletions test/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,180 @@ describe('Request', () => {
await server.stop();
});

it('ends the response when the client disconnects during the handler (aborted)', async (flags) => {

const server = Hapi.server();
const team = new Teamwork.Team();

server.route({
method: 'GET',
path: '/',
handler: async (request) => {

clientRequest.destroy();
await team.work;
return 'ok';
}
});

let ended = null;
server.ext('onRequest', (request, h) => {

const res = request.raw.res;
const end = res.end;
res.end = function (...args) {

ended = res.statusCode;
return end.apply(this, args);
};

return h.continue;
});

let disconnected = false;
server.ext('onPreHandler', (request, h) => {

request.events.once('disconnect', () => {

disconnected = true;
});

return h.continue;
});

flags.onCleanup = () => server.stop();
await server.start();

const clientRequest = Http.request({ hostname: 'localhost', port: server.info.port, method: 'GET' });
clientRequest.on('error', Hoek.ignore);
clientRequest.end();

const [request] = await server.events.once('response');
team.attend(); // Response is reported before the handler completes

expect(disconnected).to.be.true();
expect(request.response.isBoom).to.be.true();
expect(request.response.message).to.equal('Request aborted');
expect(request.response.output.statusCode).to.equal(499);
expect(ended).to.equal(499);
expect(request.raw.res.writableEnded).to.be.true();
});

it('ends the response when the client disconnects during the handler (close after payload)', async (flags) => {

const server = Hapi.server();
const team = new Teamwork.Team();

server.route({
method: 'POST',
path: '/',
options: {
response: { disconnectStatusCode: 444 },
handler: async (request) => {

clientRequest.destroy();
await team.work;
return 'ok';
}
}
});

let ended = null;
server.ext('onRequest', (request, h) => {

const res = request.raw.res;
const end = res.end;
res.end = function (...args) {

ended = res.statusCode;
return end.apply(this, args);
};

return h.continue;
});

let disconnected = false;
server.ext('onPreHandler', (request, h) => {

request.events.once('disconnect', () => {

disconnected = true;
});

return h.continue;
});

let postHandler = 0;
server.ext('onPostHandler', (request, h) => {

++postHandler;
return h.continue;
});

flags.onCleanup = () => server.stop();
await server.start();

const clientRequest = Http.request({ hostname: 'localhost', port: server.info.port, method: 'POST', headers: { 'content-type': 'application/json' } });
clientRequest.on('error', Hoek.ignore);
clientRequest.end('{"a":1}');

const [request] = await server.events.once('response');
team.attend(); // Response is reported before the handler completes

expect(request.payload).to.equal({ a: 1 }); // Payload was fully received (no 'aborted' event)
expect(disconnected).to.be.true();
expect(request.response.isBoom).to.be.true();
expect(request.response.output.statusCode).to.equal(444);
expect(ended).to.equal(444);
expect(request.raw.res.writableEnded).to.be.true();

await Hoek.wait(10);
expect(postHandler).to.equal(0);
});

it('reports disconnect when the client disconnects during onPreResponse', async (flags) => {

const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'ok' });

let ended = null;
server.ext('onRequest', (request, h) => {

const res = request.raw.res;
const end = res.end;
res.end = function (...args) {

ended = res.statusCode;
return end.apply(this, args);
};

return h.continue;
});

server.ext('onPreResponse', async (request, h) => {

const disconnect = request.events.once('disconnect');
clientRequest.destroy();
await disconnect;
return h.continue;
});

flags.onCleanup = () => server.stop();
await server.start();

const clientRequest = Http.request({ hostname: 'localhost', port: server.info.port, method: 'GET' });
clientRequest.on('error', Hoek.ignore);
clientRequest.end();

const [request] = await server.events.once('response');

expect(request.response.isBoom).to.be.true();
expect(request.response.output.statusCode).to.equal(499);
expect(request.response.data.source).to.equal('ok');
expect(ended).to.equal(499);
expect(request.info.responded).to.equal(0);
});

it('returns not found on internal only route (external)', async () => {

const server = Hapi.server();
Expand Down
Loading