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

"Cannot set headers after they are sent to the client" (ERR_HTTP_HEADERS_SENT) with AmazonDaxClient #4566

Closed
3 of 9 tasks
rchl opened this issue Feb 14, 2022 · 13 comments
Closed
3 of 9 tasks
Assignees
Labels
Meta: Help Wanted Package: node Issues related to the Sentry Node SDK Type: Bug

Comments

@rchl
Copy link
Contributor

rchl commented Feb 14, 2022

Package + Version

  • @sentry/browser
  • @sentry/node
  • raven-js
  • raven-node (raven for node)
  • other:

Version:

6.17.5

Description

This affects a complicated setup that I can't provide reproduction for easily.

The setup is:

  • and express backend with express-session middleware running
  • custom session store that retrieves and stores session in a DynamoDB database and uses AmazonDaxClient for DB operations

Steps are:

  • navigate to express route that just returns some data res.json('OK')
  • The express-middleware reads the session before handling the route and tries to save it on res.end() (which express-session wraps)
  • When the saving of the session fails the dax client emits an error event:
    Screenshot 2022-02-14 at 16 08 36
  • The emitted event seems to get handled by Sentry itself in the code linked below, resulting in the next express middleware running, even though the previous one has already responded to the request:
    }
    return function sentryRequestMiddleware(
    req: http.IncomingMessage,
    res: http.ServerResponse,
    next: (error?: any) => void,
    ): void {
    if (options && options.flushTimeout && options.flushTimeout > 0) {
    // eslint-disable-next-line @typescript-eslint/unbound-method
    const _end = res.end;
    res.end = function (chunk?: any | (() => void), encoding?: string | (() => void), cb?: () => void): void {
    void flush(options.flushTimeout)
    .then(() => {
    _end.call(this, chunk, encoding, cb);
    })
    .then(null, e => {
    logger.error(e);
    });
    };
    }
    const local = domain.create();
    local.add(req);
    local.add(res);
    local.on('error', next);
  • The difference with and without Sentry is that this.domain is not defined in the code below when Sentry is not used
    Screenshot_2022-02-14_at_16 17 17

When the issue triggers then we are getting the ERR_HTTP_HEADERS_SENT error from Node because some later error middleware runs even though we've responded already.

@AbhiPrasad
Copy link
Member

Thanks for the detailed issue. Could you provide your Sentry.init config. Are you using tracing?

I assume there's a conflict with domains here, but I'm not familiar enough with express middlewares to be certain. I'm putting this in our backlog to address, but PRs welcome for anyone who wants to help out!

@rchl
Copy link
Contributor Author

rchl commented Feb 15, 2022

The init options are not relevant because I have reproduced that even without initializing Sentry. It's just about the the Sentry requestHandler apparently.

Here is minimal reproduction that reproduces with minimal dummy express-session store:
https://github.com/rchl/sentry-bug-repro

@rchl rchl changed the title "Cannot set headers after they are sent to the client" (ERR_HTTP_HEADERS_SENT) with AmazonDaxClient & Sentry "Cannot set headers after they are sent to the client" (ERR_HTTP_HEADERS_SENT) with AmazonDaxClient Feb 15, 2022
@edennis
Copy link

edennis commented Apr 7, 2022

I'm also getting the same error. I've used git-bisect to determine that the regression was introduced in 6.14.0 (6.13.3 works fine).

@souredoutlook
Copy link
Member

@lobsterkatie Just confirming that the ongoing solution to this for affected users has been to downgrade to 6.13.3

@AbhiPrasad
Copy link
Member

Pushed this up in priority - in the mean time, PRs are welcome if any one wants to take a look and help us out!

@rchl
Copy link
Contributor Author

rchl commented Jun 30, 2022

Getting the same error doesn't mean that the root issue is the same. This error can trigger for various reasons.

I don't quite see how my specific issue could be fixed without maybe refactoring how things fundamentally work.

@timfish
Copy link
Collaborator

timfish commented Oct 24, 2022

I just tested your sentry-bug-repro example with 7.16.0 and since #5627, next is no longer called and because the domain error is no longer handled, the process terminates.

The output I get is:

> node src/index.mjs

Example app listening at http://localhost:8123
node:events:491
      throw er; // Unhandled 'error' event
      ^

Error: Dummy Error
    at RequestObject.emitError (file:///Users/tim/Documents/Repositories/sentry-bug-repro-main/src/session-store.mjs:6:28)
    at Timeout._onTimeout (file:///Users/tim/Documents/Repositories/sentry-bug-repro-main/src/session-store.mjs:21:25)
    at listOnTimeout (node:internal/timers:564:17)
    at process.processTimers (node:internal/timers:507:7)
Emitted 'error' event on Domain instance at:
    at RequestObject.emit (node:domain:540:12)
    at RequestObject.emitError (file:///Users/tim/Documents/Repositories/sentry-bug-repro-main/src/session-store.mjs:6:14)
    at Timeout._onTimeout (file:///Users/tim/Documents/Repositories/sentry-bug-repro-main/src/session-store.mjs:21:25)
    at listOnTimeout (node:internal/timers:564:17)
    at process.processTimers (node:internal/timers:507:7) {
  domainEmitter: RequestObject {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    [Symbol(kCapture)]: false
  },
  domainThrown: false
}

Node.js v18.9.0

@timfish
Copy link
Collaborator

timfish commented Oct 24, 2022

@AbhiPrasad can probably confirm, but I think this is blocked on async_hooks support (#3660).

@AbhiPrasad
Copy link
Member

Can folks not explicitly catch and manage the error here? The issue with domain and next() was that we were incorrectly forwarding it.

@timfish
Copy link
Collaborator

timfish commented Oct 24, 2022

It's happening in some third-party middleware.

I suppose I can try and wrap our call to next() in the domain. Let me try that....

@timfish
Copy link
Collaborator

timfish commented Oct 24, 2022

I suppose I can try and wrap our call to next() in the domain. Let me try that....

There is no way to do this unfortunately since next is sync and the error is thrown in an async context.

custom session store that retrieves and stores session in a DynamoDB database and uses AmazonDaxClient for DB operations

@rchl I changed your code to catch the error and pass it onto the callback like this, rather than allow an unhandledrejection:

  set(sid, data, cb) {
    const request = new RequestObject();
    new Promise((resolve) => {
      // Delaying a bit to also trigger the ERR_HTTP_HEADERS_SENT error.
      setTimeout(() => {
        try {
          request.emitError();
          cb();
        } catch (e) {
          cb(e);
        }
      });
    });
  }

With 7.16.0, I get the following output, even when NOT using the Sentry handler:

Example app listening at http://localhost:8123
THIS ERROR HANDLER SHOULD NOT RUN!
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at new NodeError (node:internal/errors:393:5)
    at ServerResponse.setHeader (node:_http_outgoing:607:11)
    at ServerResponse.header (/Users/tim/Documents/Repositories/sentry-bug-repro-main/node_modules/express/lib/response.js:776:10)
    at ServerResponse.contentType (/Users/tim/Documents/Repositories/sentry-bug-repro-main/node_modules/express/lib/response.js:604:15)
    at ServerResponse.send (/Users/tim/Documents/Repositories/sentry-bug-repro-main/node_modules/express/lib/response.js:145:14)
    at file:///Users/tim/Documents/Repositories/sentry-bug-repro-main/src/index.mjs:23:7
    at Layer.handle_error (/Users/tim/Documents/Repositories/sentry-bug-repro-main/node_modules/express/lib/router/layer.js:71:5)
    at trim_prefix (/Users/tim/Documents/Repositories/sentry-bug-repro-main/node_modules/express/lib/router/index.js:321:13)
    at /Users/tim/Documents/Repositories/sentry-bug-repro-main/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/Users/tim/Documents/Repositories/sentry-bug-repro-main/node_modules/express/lib/router/index.js:341:12)

@timfish
Copy link
Collaborator

timfish commented Oct 24, 2022

I've just tested it and the ERR_HTTP_HEADERS_SENT error is actually caused by the call to res.send("FAIL"). Due to the async nature of the error and the fact that you've already called res.status(200).send("Hello World!").

The assertion that THIS ERROR HANDLER SHOULD NOT RUN is not true. All error handlers are called in order of registration.

So to fix your issue:

  • Update to the latest version of the SDK
  • Catch async errors and pass them to the callback:
        try {
          request.emitError();
          cb();
        } catch (e) {
          cb(e);
        }
  • You can't send after previously res.status().send()

Please feel free to re-open this issue if you have any new information or you believe the above is not true.

@timfish timfish closed this as completed Oct 24, 2022
@rchl
Copy link
Contributor Author

rchl commented Oct 24, 2022

@timfish I think this alone fixes the whole issue:

I just tested your sentry-bug-repro example with 7.16.0 and since #5627, next is no longer called and because the domain error is no longer handled, the process terminates.

I could discuss other points that you've raised but I think it's not really relevant given the above.
I think other issues that remain after Sentry is no longer listening to domain error would be in either dax client or express-session.

Thanks for handling it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Meta: Help Wanted Package: node Issues related to the Sentry Node SDK Type: Bug
Projects
None yet
Development

No branches or pull requests

7 participants