got icon indicating copy to clipboard operation
got copied to clipboard

The onCancel handler was attached after the promise settled

Open yovanoc opened this issue 5 years ago • 59 comments

Describe the bug

  • Node.js version: 14.13
  • OS & version: MacOS Catalina

The onCancel handler was attached after the promise settled

Actual behavior

This unhandled exception is not catch by a try/catch, but I see that it happen after a timeout error like this issue #1460

Checklist

  • [x] I have read the documentation.
  • [x] I have tried my code with the latest version of Node.js and Got.

yovanoc avatar Oct 06 '20 06:10 yovanoc

Can you please post some code?

szmarczak avatar Oct 06 '20 07:10 szmarczak

Node.js version: 15.1.0 OS & version: Windows 10

Just encountered the same, very hard to repro, looks like a race-condition, I will try to dig deeper, but is there any way to catch such exception and just keep running?

Stack:

error: unhandledRejection: The `onCancel` handler was attached after the promise settled.
Error: The `onCancel` handler was attached after the promise settled.
    at onCancel (scripts\node_modules\p-cancelable\index.js:46:12)
    at makeRequest (scripts\node_modules\got\dist\source\as-promise\index.js:38:13)
    at Request.<anonymous> (scripts\node_modules\got\dist\source\as-promise\index.js:143:17)
    at Object.onceWrapper (node:events:434:26)
    at Request.emit (node:events:327:20)
    at Timeout.retry (scripts\node_modules\got\dist\source\core\index.js:1261:30)
    at listOnTimeout (node:internal/timers:555:17)
    at processTimers (node:internal/timers:498:7) 

bkarlson avatar Nov 10 '20 09:11 bkarlson

Thanks for the report @bkarlson. Just to confirm - are you running Got 11.8.0?

szmarczak avatar Nov 10 '20 12:11 szmarczak

Yes, 11.8.0, and the issue seems to be related to retry logic, because if I disable it (retry: 0) the error never happens. If I have retry: 3 with other retry options set to default, Got will throw this exception at some point.

bkarlson avatar Nov 10 '20 13:11 bkarlson

An easy fix would be using if (!thePromise._isPending) before setting the onCancel handlers, but it's undocumented.

I'm still investigating on the root cause.

Giotino avatar Nov 12 '20 11:11 Giotino

This is still happening.

Here my error trace: https://sentry.io/share/issue/6f578a2a936943cca5afe367e6732995/

Kikobeats avatar Feb 23 '21 08:02 Kikobeats

@yovanoc @Kikobeats @bkarlson Can you try again? The p-cancelable package has received a neat fix that could potentially fix this error. Make sure to update the npm dependencies.

szmarczak avatar Mar 10 '21 08:03 szmarczak

@szmarczak I'm still experimenting the error

https://sentry.io/share/issue/c607e343ef6345c38ad391a7c3f43149/

node version is: v14.16.0 got version is: 11.8.2 p-cancelable: 2.1.0

Kikobeats avatar Mar 14 '21 09:03 Kikobeats

@Kikobeats Can you show the line(s) where you call got? Do you pass any hooks? Do you attach any event handlers?

szmarczak avatar Mar 14 '21 17:03 szmarczak

@Kikobeats can you replace dist/as-promise/index.js with https://gist.github.com/szmarczak/310ee80c1085bdacd042c64fd992ef76 ? I made some changes so it logs what happened.

szmarczak avatar Mar 14 '21 18:03 szmarczak

@bkarlson @yovanoc Can you too try the above?

szmarczak avatar Mar 14 '21 18:03 szmarczak

@szmarczak In my case is hard to say since there are a couple of dependencies using got and the stack trace is not revealing the details about which the origin of the problem.

Also, since this is running a production environment, I can't replace it in an easy way.

Maybe can you release a got RC version so I can force to be used?

Kikobeats avatar Mar 15 '21 10:03 Kikobeats

~~I have an idea how to reproduce this. request.destroy() may be called, the promise may be canceled, but request hasn't been emitted yet and there is no check if it has been destroyed. Will check this.~~

szmarczak avatar Mar 23 '21 16:03 szmarczak

Actually no because it's destroyed here https://github.com/sindresorhus/got/blob/4cce4de19ed577f481ba45a023b395a29cadce86/source/core/index.ts#L1469-L1472

szmarczak avatar Mar 23 '21 17:03 szmarczak

~~I think I finally may be able to reproduce this.~~ Actually I can't figure this out :( This is definitely not via afterResponse hook. This must be related to timeouts.

szmarczak avatar Mar 24 '21 02:03 szmarczak

@Kikobeats What is the timeout event? Is it request event? It'd be best if you shared the entire timeout error, including timeoutError.timings.

szmarczak avatar Mar 24 '21 03:03 szmarczak

Also can you do npm ls got and show me which versions are being used?

szmarczak avatar Mar 24 '21 04:03 szmarczak

@Kikobeats @bkarlson @yovanoc If your project uses Got directly you can try

const isResponseOk = response => {
	const {statusCode} = response;
	const limitStatusCode = response.request.options.followRedirect ? 299 : 399;

	return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304;
};

const instance = got.extend({
    hooks: {
        afterResponse: [
            response => {
                if (isResponseOk(response)) {
                    response.request.destroy();
                }
            
                return response;
            }
        ]
    }
});

If so, please let me know if that fixes the issue.

szmarczak avatar Mar 24 '21 04:03 szmarczak

Hey @szmarczak, I'm running your code snippet on my production servers since yesterday and I didn't experiment errors until now (maybe it needs a bit of extra time)

Kikobeats avatar Apr 02 '21 08:04 Kikobeats

If so, please let me know if that fixes the issue.

That one fixed the issue for me as well. Thank you very much @szmarczak ! 🙏🏻

I am running got as part of generating a fake environment with lots of pictures. I have a feeling many uploads in quick succession are causing this.

CvBlixen avatar Apr 08 '21 10:04 CvBlixen

@CvBlixen Yes! If you have set the response timeout and the request payload has been fully written after the response event has been emitted, this bug can occur.

It can also happen with a read timeout, when the response (not the request) emits an error.

szmarczak avatar Apr 08 '21 17:04 szmarczak

The issue can also occur when a send timeout is set and the connection fails. socket.writable check is missing in https://github.com/sindresorhus/got/blob/4cce4de19ed577f481ba45a023b395a29cadce86/source/core/utils/timed-out.ts#L157

Actually no, because request is destroyed on error.

szmarczak avatar Apr 08 '21 17:04 szmarczak

Also the secureConnect timeout should not run if rejectUnauthorized is set to false.

szmarczak avatar Apr 08 '21 18:04 szmarczak

I wonder if this check https://github.com/sindresorhus/got/blob/4cce4de19ed577f481ba45a023b395a29cadce86/source/core/utils/timed-out.ts#L135 can be replaced with socket.connecting.

szmarczak avatar Apr 08 '21 18:04 szmarczak

request timeout can also cause this. For example when the error is emitted on response instead. Another possibility could be that request.response is already defined when addTimeout is ran.

szmarczak avatar Apr 08 '21 18:04 szmarczak

@CvBlixen @bkarlson Before you applied the fix, were you getting TimeoutErrors as well? If so, can you share the stack trace along with the message please?

szmarczak avatar Apr 08 '21 18:04 szmarczak

I'm not sure if this will help, but I think I'm seeing this. I've been attempting to troubleshoot why a network interruption causes my code (which uses ring-client-api which uses got) stops updating even after the network connection is reestablished, but only when using http2. I modified ring-client-api to set the timeout to 10 seconds, and then I interrupt the network connection to create the timeout but I get this which I can't seem to trap:

(node:69900) UnhandledPromiseRejectionWarning: Error: The `onCancel` handler was attached after the promise settled.
    at onCancel (/opt/ring-mqtt/node_modules/ring-client-api/node_modules/p-cancelable/index.js:48:12)
    at makeRequest (/opt/ring-mqtt/node_modules/ring-client-api/node_modules/got/dist/source/as-promise/index.js:38:13)
    at Request.<anonymous> (/opt/ring-mqtt/node_modules/ring-client-api/node_modules/got/dist/source/as-promise/index.js:143:17)
    at Object.onceWrapper (events.js:422:26)
    at Request.emit (events.js:315:20)
    at Timeout.retry (/opt/ring-mqtt/node_modules/ring-client-api/node_modules/got/dist/source/core/index.js:1264:30)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:69900) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:69900) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
events.js:292
      throw er; // Unhandled 'error' event
      ^

TimeoutError: Timeout awaiting 'request' for 10000ms
    at Timeout.timeoutHandler [as _onTimeout] (/opt/ring-mqtt/node_modules/ring-client-api/node_modules/got/dist/source/core/utils/timed-out.js:36:25)
    at listOnTimeout (internal/timers.js:556:17)
    at processTimers (internal/timers.js:497:7)
Emitted 'error' event on Request instance at:
    at emitErrorNT (internal/streams/destroy.js:106:8)
    at errorOrDestroy (internal/streams/destroy.js:168:7)
    at internal/streams/writable.js:613:7
    at /opt/ring-mqtt/node_modules/ring-client-api/node_modules/got/dist/source/core/index.js:1348:17
    at ClientRequest.onerror (internal/streams/writable.js:680:5)
    at ClientRequest.emit (events.js:327:22)
    at ClientRequest.origin.emit (/opt/ring-mqtt/node_modules/ring-client-api/node_modules/@szmarczak/http-timer/dist/source/index.js:39:20)
    at emitErrorNT (internal/streams/destroy.js:106:8)
    at emitErrorCloseNT (internal/streams/destroy.js:74:3)
    at processTicksAndRejections (internal/process/task_queues.js:80:21) {
  event: 'request',
  code: 'ETIMEDOUT'
}

Any help would be appreciated.

tsightler avatar Apr 20 '21 02:04 tsightler

@tsightler So... are you able to reproduce this?

szmarczak avatar Apr 20 '21 08:04 szmarczak

Yes, 100% reproducible if I have an active HTTP2 session with a configured timeout and then block the internet connection to simulate a communication failure. This produces a request timeout as shown above. I can somewhat overcome the issue if I set retry: 0, as this seems to suppress the error, although then I get a different error from http2-wrapper, although that one doesn't crash the process so I'm thinking I may be able to work around it.

I'm admittedly super new to all of this as normally the underlying interactions with HTTP are below the code I maintain. The API I use recently switched to using HTTP2 by default for the connection to the servers, and this has introduced the issue of failure to reconnect. I did notice that you say that HTTP2 is only supported for Node 15, so that's interesting, although I did try Node 15, but the timeout error above persisted.

tsightler avatar Apr 20 '21 14:04 tsightler

Got this bug today after migrating to esm. Not sure if it's related but this code's worked for months

Allain55 avatar May 19 '21 14:05 Allain55