Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds fallback logic for AsyncLocalStorage (ALS) snapshots in serverless environments like Cloudflare Workers. The issue occurs when ALS snapshots become stale after the request context in which they were created ends, causing "Cannot call this AsyncLocalStorage bound function outside of the request" errors.
Changes:
- Replaced direct snapshot assignment with a wrapper function that implements retry logic with fallback
- Added helper functions
getAsyncLocalStorageSnapshot()andisAsyncLocalStorageError()to modularize snapshot creation and error detection - Renamed type alias from
_INTERNAL_RandomSafeContextRunnertoRandomSafeContextRunnerfor cleaner local naming
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| function isAsyncLocalStorageError(error: unknown): boolean { | ||
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); |
There was a problem hiding this comment.
The error detection logic is too broad and may incorrectly catch errors that merely mention "AsyncLocalStorage" in their message but aren't the specific ALS binding error that needs to be handled. Consider checking for the exact error message: "Cannot call this AsyncLocalStorage bound function outside of the request in which it was created." This will prevent false positives where unrelated errors that happen to mention AsyncLocalStorage in their message would incorrectly trigger the retry/fallback logic.
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); | |
| return ( | |
| error instanceof Error && | |
| error.message === | |
| 'Cannot call this AsyncLocalStorage bound function outside of the request in which it was created.' | |
| ); |
There was a problem hiding this comment.
Exact error matching is flimsy IMO, also I am not sure if it would be the exact same error in each serverless environment.
There was a problem hiding this comment.
Also not sure what would be the best call here, I think just AsyncLocalStorage is fine when thrown at this point
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
chargome
left a comment
There was a problem hiding this comment.
Thanks for updating! I guess there is no way to properly reflect this in a e2e test so lgtm!
| throw error; | ||
| } | ||
|
|
||
| // Snapshot likely stale, try to get a fresh one and retry |
There was a problem hiding this comment.
How can the snapshot get stale?
There was a problem hiding this comment.
Perhaps it's not the best way to describe this, but this was the closest analogy. This is what I found while debugging, when we set the ALS snapshot on startup:
- if cold start it gets initialised again and it works as expected, at that point the snapshot isn't set, so the callbacks execute normally.
- if warm start, the global already exists, so some random APIs might run before the
inittakes effect. It will run with the previous snapshot that was set which would be in a different request context.
Or that's what I think is happening.
| } | ||
|
|
||
| function isAsyncLocalStorageError(error: unknown): boolean { | ||
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); |
There was a problem hiding this comment.
Also not sure what would be the best call here, I think just AsyncLocalStorage is fine when thrown at this point
Summary
This was introduced by cache components workaround in #18700.
When deploying a Next.js app on Cloudflare Workers and I presume other serverless environments, you would get this error
This happens because the snapshot becomes stale in the next request invocation.
Solution
This is more of a best-effort workaround. I added a retry logic based on some criteria, if the first attempt fails due to ALS error then it will grab a new snapshot and re-run it, if that also fails, then it will run the callback directly.
the flow goes like this:
flowchart TD A[callback invoked] --> B{Try cached snapshot} B -->|Success| C[Return result] B -->|Error| D{Is AsyncLocalStorage error?} D -->|No| E[Rethrow error] D -->|Yes| F[Get fresh snapshot] F --> G{Fresh snapshot available?} G -->|No| H[Execute callback directly] G -->|Yes| I[Update cached snapshot] I --> J{Try fresh snapshot} J -->|Success| K[Return result] J -->|Error| L{Is AsyncLocalStorage error?} L -->|No| M[Rethrow error] L -->|Yes| N[Execute callback directly] H --> O[Return result] N --> O style C fill:#90EE90 style K fill:#90EE90 style O fill:#90EE90 style E fill:#FFB6C1 style M fill:#FFB6C1Closes #18842