Skip to content

Commit 5dfe759

Browse files
committed
fix: implement ALS runner fallback for cloudflare workers support
1 parent d503a8d commit 5dfe759

1 file changed

Lines changed: 63 additions & 9 deletions

File tree

packages/nextjs/src/server/prepareSafeIdGeneratorContext.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
type _INTERNAL_RandomSafeContextRunner as _INTERNAL_RandomSafeContextRunner,
2+
type _INTERNAL_RandomSafeContextRunner as RandomSafeContextRunner,
33
debug,
44
GLOBAL_OBJ,
55
} from '@sentry/core';
@@ -15,17 +15,53 @@ type OriginalAsyncLocalStorage = typeof AsyncLocalStorage;
1515
*/
1616
export function prepareSafeIdGeneratorContext(): void {
1717
const sym = Symbol.for('__SENTRY_SAFE_RANDOM_ID_WRAPPER__');
18-
const globalWithSymbol: typeof GLOBAL_OBJ & { [sym]?: _INTERNAL_RandomSafeContextRunner } = GLOBAL_OBJ;
19-
const als = getAsyncLocalStorage();
20-
if (!als || typeof als.snapshot !== 'function') {
21-
DEBUG_BUILD &&
22-
debug.warn(
23-
'[@sentry/nextjs] No AsyncLocalStorage found in the runtime or AsyncLocalStorage.snapshot() is not available, skipping safe random ID generator context preparation, you may see some errors with cache components.',
24-
);
18+
const globalWithSymbol: typeof GLOBAL_OBJ & { [sym]?: RandomSafeContextRunner } = GLOBAL_OBJ;
19+
20+
// Get initial snapshot - if unavailable, don't set up the wrapper at all
21+
const initialSnapshot = getAsyncLocalStorageSnapshot();
22+
if (!initialSnapshot) {
2523
return;
2624
}
2725

28-
globalWithSymbol[sym] = als.snapshot();
26+
// We store a wrapper function instead of the raw snapshot because in serverless
27+
// environments (e.g., Cloudflare Workers), the snapshot is bound to the request
28+
// context it was created in. Once that request ends, the snapshot becomes invalid.
29+
// The wrapper catches this and creates a fresh snapshot for the current request context.
30+
let cachedSnapshot: RandomSafeContextRunner = initialSnapshot;
31+
32+
globalWithSymbol[sym] = <T>(callback: () => T): T => {
33+
try {
34+
return cachedSnapshot(callback);
35+
} catch (error) {
36+
// Only handle AsyncLocalStorage-related errors, rethrow others
37+
if (!isAsyncLocalStorageError(error)) {
38+
throw error;
39+
}
40+
41+
// Snapshot likely stale, try to get a fresh one and retry
42+
const freshSnapshot = getAsyncLocalStorageSnapshot();
43+
// No snapshot available, fall back to direct execution
44+
if (!freshSnapshot) {
45+
return callback();
46+
}
47+
48+
// Update the cached snapshot
49+
cachedSnapshot = freshSnapshot;
50+
51+
// Retry the callback with the fresh snapshot
52+
try {
53+
return cachedSnapshot(callback);
54+
} catch (retryError) {
55+
// Only fall back for AsyncLocalStorage errors, rethrow others
56+
if (!isAsyncLocalStorageError(retryError)) {
57+
throw retryError;
58+
}
59+
// If fresh snapshot also fails with ALS error, fall back to direct execution
60+
return callback();
61+
}
62+
}
63+
};
64+
2965
DEBUG_BUILD && debug.log('[@sentry/nextjs] Prepared safe random ID generator context');
3066
}
3167

@@ -47,3 +83,21 @@ function getAsyncLocalStorage(): OriginalAsyncLocalStorage | undefined {
4783

4884
return undefined;
4985
}
86+
87+
function getAsyncLocalStorageSnapshot(): RandomSafeContextRunner | undefined {
88+
const als = getAsyncLocalStorage();
89+
90+
if (!als || typeof als.snapshot !== 'function') {
91+
DEBUG_BUILD &&
92+
debug.warn(
93+
'[@sentry/nextjs] No AsyncLocalStorage found in the runtime or AsyncLocalStorage.snapshot() is not available, skipping safe random ID generator context preparation, you may see some errors with cache components.',
94+
);
95+
return undefined;
96+
}
97+
98+
return als.snapshot();
99+
}
100+
101+
function isAsyncLocalStorageError(error: unknown): boolean {
102+
return error instanceof Error && error.message.includes('AsyncLocalStorage');
103+
}

0 commit comments

Comments
 (0)