Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix the headersSent check and avoid Error: write EPIPE #79

Closed
wants to merge 1 commit into from
Closed
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 index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ declare namespace errorHandlerFactory {
* @param req Incoming request
* @param res Response
* @param options Options for error handler settings
* @returns false if the response has been sent before the error
*/
function writeErrorToResponse(
err: Error,
req: Express.Request,
res: Express.Response,
options?: ErrorWriterOptions
): void;
): boolean;

/**
* Error-handling middleware function. Includes server-side logging
Expand Down
11 changes: 7 additions & 4 deletions lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ var SG = require('strong-globalize');
SG.SetRootDir(path.resolve(__dirname, '..'));
var buildResponseData = require('./data-builder');
var debug = require('debug')('strong-error-handler');
var format = require('util').format;
var logToConsole = require('./logger');
var negotiateContentProducer = require('./content-negotiation');

Expand Down Expand Up @@ -50,9 +49,12 @@ function writeErrorToResponse(err, req, res, options) {

options = options || {};

if (res._header) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're removing the case for res._header because it no longer exists and replaced by headersSent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headersSent is the documented api while _header is an internal property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to use res.headersSent

debug('Response was already sent, closing the underlying connection');
return req.socket.destroy();
// See https://nodejs.org/api/http.html#http_response_headerssent
if (res.headersSent) {
debug('Response was already sent. Skipping error response.');
// We should not destroy the request socket as it causes Error: write EPIPE

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, the EPIPE error is still happening without destroying the request socket. Do we want to throw a meaningful error here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The EPIPE error is reported from the client side as the socket is destroyed by the server.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, any idea on why it's failing for me on node 10?

  49 passing (236ms)
  1 failing

  1) strong-error-handler
       handles response headers already sent without destroying the request:
     Error: write EPIPE
      at WriteWrap.afterWrite [as oncomplete] (net.js:788:14)



npm ERR! Test failed.  See above for more details.
biniams-mbp:strong-error-handler badmike$ node -v
v10.11.0
biniams-mbp:strong-error-handler badmike$ npm -v
6.4.1
biniams-mbp:strong-error-handler badmike$ git br
* fix-write-error
  master

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit more verbose logs:

biniams-mbp:strong-error-handler badmike$ DEBUG=strong-error-handler npm t

> strong-error-handler@3.2.0 test /Users/badmike/loopback/strong-error-handler> mocha



  strong-error-handler
  strong-error-handler Initializing with options {"log":false} +0ms
  strong-error-handler Handling Error: an error
    at givenErrorHandlerForError (/Users/badmike/loopback/strong-error-handler/test/handler.test.js
:859:23)
    at Context.<anonymous> (/Users/badmike/loopback/strong-error-handler/test/handler.test.js:43:7)
    at callFnAsync (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runnable.js
:400:21)
    at Test.Runnable.run (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runna
ble.js:342:7)
    at Runner.runTest (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.j
s:455:10)
    at /Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.js:573:12
    at next (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.js:369:14)
    at /Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.js:379:7
    at next (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.js:303:14)
    at /Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.js:342:7
    at done (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runnable.js:319:5)
    at callFn (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runnable.js:395:7)
    at Hook.Runnable.run (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runnable.js:364:7)
    at next (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.js:317:10)
    at Immediate.<anonymous> (/Users/badmike/loopback/strong-error-handler/node_modules/mocha/lib/runner.js:347:5)
    at runCallback (timers.js:694:18)
    at tryOnImmediate (timers.js:665:5)
    at processImmediate (timers.js:647:5) +40ms
  strong-error-handler Response was already sent. Skipping error response. +1ms
    1) handles response headers already sent without destroying the request


  0 passing (71ms)
  1 failing

  1) strong-error-handler
       handles response headers already sent without destroying the request:
     Error: write EPIPE
      at WriteWrap.afterWrite [as oncomplete] (net.js:788:14)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it now. It's interesting, res.end should half-close the socket and it should allow in-progress request to finish.

// return req.socket.destroy();
return false;
}

// this will alter the err object, to handle when res.statusCode is an error
Expand All @@ -67,6 +69,7 @@ function writeErrorToResponse(err, req, res, options) {

var sendResponse = negotiateContentProducer(req, warn, options);
sendResponse(res, data);
return true;

function warn(msg) {
res.header('X-Warning', msg);
Expand Down
23 changes: 23 additions & 0 deletions test/handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ describe('strong-error-handler', function() {
request.get('/').expect(200, 'empty', done);
});

it('handles response headers already sent without destroying the request',
function(done) {
givenErrorHandlerForError();
var handler = _requestHandler;
_requestHandler = function(req, res, next) {
res.end('empty');
process.nextTick(function() {
handler(req, res, next);
});
};
request.post('/').send(givenLargeRequest())
.expect(200, 'empty', function(err) {
// Skip EPIPE
if (err && err.code !== 'EPIPE') return done(err);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am afraid this test does not fail with the old implementation either. I am concerned that this test may never fail, which would give us a false sense of security.

else return done();
});
});

context('status code', function() {
it('converts non-error "err.status" to 500', function(done) {
givenErrorHandlerForError(new ErrorWithProps({status: 200}));
Expand Down Expand Up @@ -919,3 +937,8 @@ function getExpectedErrorData(err) {
cloneAllProperties(data, err);
return data;
}

function givenLargeRequest() {
const data = Buffer.alloc(2 * 1024 * 1024, 'A', 'utf-8');
return data.toString();
}