-
-
Couldn't load subscription status.
- Fork 1.7k
fix(cloudflare): copy execution context in durable objects and handlers #17786
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
Changes from all commits
dfbd1ca
aae2dd3
5854923
c0230ea
6e62674
7121b65
224a1d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,3 +61,6 @@ packages/gatsby/gatsby-node.d.ts | |
| # intellij | ||
| *.iml | ||
| /**/.wrangler/* | ||
|
|
||
| #junit reports | ||
| packages/**/*.junit.xml | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { getFinalOptions } from './options'; | |
| import { wrapRequestHandler } from './request'; | ||
| import { addCloudResourceContext } from './scope-utils'; | ||
| import { init } from './sdk'; | ||
| import { copyExecutionContext } from './utils/copyExecutionContext'; | ||
|
|
||
| /** | ||
| * Wrapper for Cloudflare handlers. | ||
|
|
@@ -38,7 +39,9 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM | |
| if ('fetch' in handler && typeof handler.fetch === 'function' && !isInstrumented(handler.fetch)) { | ||
| handler.fetch = new Proxy(handler.fetch, { | ||
| apply(target, thisArg, args: Parameters<ExportedHandlerFetchHandler<Env, CfHostMetadata>>) { | ||
| const [request, env, context] = args; | ||
| const [request, env, ctx] = args; | ||
| const context = copyExecutionContext(ctx); | ||
| args[2] = context; | ||
|
|
||
| const options = getFinalOptions(optionsCallback(env), env); | ||
|
|
||
|
|
@@ -72,7 +75,10 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM | |
| if ('scheduled' in handler && typeof handler.scheduled === 'function' && !isInstrumented(handler.scheduled)) { | ||
| handler.scheduled = new Proxy(handler.scheduled, { | ||
| apply(target, thisArg, args: Parameters<ExportedHandlerScheduledHandler<Env>>) { | ||
| const [event, env, context] = args; | ||
| const [event, env, ctx] = args; | ||
| const context = copyExecutionContext(ctx); | ||
| args[2] = context; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just leaving a note here for the record. Here we mutate the actual args directly and is often not safe to use, but since we are inside a Proxy it is fine and also a bit faster :) |
||
|
|
||
| return withIsolationScope(isolationScope => { | ||
| const options = getFinalOptions(optionsCallback(env), env); | ||
| const waitUntil = context.waitUntil.bind(context); | ||
|
|
@@ -115,7 +121,10 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM | |
| if ('email' in handler && typeof handler.email === 'function' && !isInstrumented(handler.email)) { | ||
| handler.email = new Proxy(handler.email, { | ||
| apply(target, thisArg, args: Parameters<EmailExportedHandler<Env>>) { | ||
| const [emailMessage, env, context] = args; | ||
| const [emailMessage, env, ctx] = args; | ||
| const context = copyExecutionContext(ctx); | ||
| args[2] = context; | ||
|
|
||
| return withIsolationScope(isolationScope => { | ||
| const options = getFinalOptions(optionsCallback(env), env); | ||
| const waitUntil = context.waitUntil.bind(context); | ||
|
|
@@ -156,7 +165,9 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM | |
| if ('queue' in handler && typeof handler.queue === 'function' && !isInstrumented(handler.queue)) { | ||
| handler.queue = new Proxy(handler.queue, { | ||
| apply(target, thisArg, args: Parameters<ExportedHandlerQueueHandler<Env, QueueHandlerMessage>>) { | ||
| const [batch, env, context] = args; | ||
| const [batch, env, ctx] = args; | ||
| const context = copyExecutionContext(ctx); | ||
| args[2] = context; | ||
|
|
||
| return withIsolationScope(isolationScope => { | ||
| const options = getFinalOptions(optionsCallback(env), env); | ||
|
|
@@ -206,7 +217,9 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM | |
| if ('tail' in handler && typeof handler.tail === 'function' && !isInstrumented(handler.tail)) { | ||
| handler.tail = new Proxy(handler.tail, { | ||
| apply(target, thisArg, args: Parameters<ExportedHandlerTailHandler<Env>>) { | ||
| const [, env, context] = args; | ||
| const [, env, ctx] = args; | ||
| const context = copyExecutionContext(ctx); | ||
| args[2] = context; | ||
|
|
||
| return withIsolationScope(async isolationScope => { | ||
| const options = getFinalOptions(optionsCallback(env), env); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { type DurableObjectState, type ExecutionContext } from '@cloudflare/workers-types'; | ||
|
|
||
| type ContextType = ExecutionContext | DurableObjectState; | ||
| type OverridesStore<T extends ContextType> = Map<keyof T, (...args: unknown[]) => unknown>; | ||
|
|
||
| /** | ||
| * Creates a new copy of the given execution context, optionally overriding methods. | ||
| * | ||
| * @param {ContextType|void} ctx - The execution context to be copied. Can be of type `ContextType` or `void`. | ||
| * @return {ContextType|void} A new execution context with the same properties and overridden methods if applicable. | ||
| */ | ||
| export function copyExecutionContext<T extends ContextType>(ctx: T): T { | ||
0xbad0c0d3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!ctx) return ctx; | ||
|
|
||
| const overrides: OverridesStore<T> = new Map(); | ||
| const contextPrototype = Object.getPrototypeOf(ctx); | ||
| const prototypeMethodNames = Object.getOwnPropertyNames(contextPrototype) as unknown as (keyof T)[]; | ||
| const ownPropertyNames = Object.getOwnPropertyNames(ctx) as unknown as (keyof T)[]; | ||
| const instrumented = new Set<unknown>(['constructor']); | ||
| const descriptors = [...ownPropertyNames, ...prototypeMethodNames].reduce((prevDescriptors, methodName) => { | ||
| if (instrumented.has(methodName)) return prevDescriptors; | ||
| if (typeof ctx[methodName] !== 'function') return prevDescriptors; | ||
| instrumented.add(methodName); | ||
| const overridableDescriptor = makeOverridableDescriptor(overrides, ctx, methodName); | ||
| return { | ||
| ...prevDescriptors, | ||
| [methodName]: overridableDescriptor, | ||
| }; | ||
| }, {}); | ||
|
|
||
| return Object.create(ctx, descriptors); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Override System Excludes Own Properties and SymbolsThe |
||
| } | ||
|
|
||
| /** | ||
| * Creates a property descriptor that allows overriding of a method on the given context object. | ||
| * | ||
| * This descriptor supports property overriding with functions only. It delegates method calls to | ||
| * the provided store if an override exists or to the original method on the context otherwise. | ||
| * | ||
| * @param {OverridesStore<ContextType>} store - The storage for overridden methods specific to the context type. | ||
| * @param {ContextType} ctx - The context object that contains the method to be overridden. | ||
| * @param {keyof ContextType} method - The method on the context object to create the overridable descriptor for. | ||
| * @return {PropertyDescriptor} A property descriptor enabling the overriding of the specified method. | ||
| */ | ||
| function makeOverridableDescriptor<T extends ContextType>( | ||
| store: OverridesStore<T>, | ||
| ctx: T, | ||
| method: keyof T, | ||
| ): PropertyDescriptor { | ||
| return { | ||
| configurable: true, | ||
| enumerable: true, | ||
| set: newValue => { | ||
| if (typeof newValue == 'function') { | ||
| store.set(method, newValue); | ||
| return; | ||
| } | ||
| Reflect.set(ctx, method, newValue); | ||
| }, | ||
0xbad0c0d3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| get: () => { | ||
| if (store.has(method)) return store.get(method); | ||
| const methodFunction = Reflect.get(ctx, method); | ||
| if (typeof methodFunction !== 'function') return methodFunction; | ||
| // We should do bind() to make sure that the method is bound to the context object - otherwise it will not work | ||
| return methodFunction.bind(ctx); | ||
| }, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { type Mocked, describe, expect, it, vi } from 'vitest'; | ||
| import { copyExecutionContext } from '../src/utils/copyExecutionContext'; | ||
|
|
||
| describe('Copy of the execution context', () => { | ||
| describe.for([ | ||
| 'waitUntil', | ||
| 'passThroughOnException', | ||
| 'acceptWebSocket', | ||
| 'blockConcurrencyWhile', | ||
| 'getWebSockets', | ||
| 'arbitraryMethod', | ||
| 'anythingElse', | ||
| ])('%s', method => { | ||
| it('Override without changing original', async () => { | ||
| const context = { | ||
| [method]: vi.fn(), | ||
| } as any; | ||
| const copy = copyExecutionContext(context); | ||
| copy[method] = vi.fn(); | ||
| expect(context[method]).not.toBe(copy[method]); | ||
| }); | ||
|
|
||
| it('Overridden method was called', async () => { | ||
| const context = { | ||
| [method]: vi.fn(), | ||
| } as any; | ||
| const copy = copyExecutionContext(context); | ||
| const overridden = vi.fn(); | ||
| copy[method] = overridden; | ||
| copy[method](); | ||
| expect(overridden).toBeCalled(); | ||
| expect(context[method]).not.toBeCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| it('No side effects', async () => { | ||
| const context = makeExecutionContextMock(); | ||
| expect(() => copyExecutionContext(Object.freeze(context))).not.toThrow( | ||
| /Cannot define property \w+, object is not extensible/, | ||
| ); | ||
| }); | ||
| it('Respects symbols', async () => { | ||
| const s = Symbol('test'); | ||
| const context = makeExecutionContextMock<ExecutionContext & { [s]: unknown }>(); | ||
| context[s] = {}; | ||
| const copy = copyExecutionContext(context); | ||
| expect(copy[s]).toBe(context[s]); | ||
| }); | ||
| }); | ||
|
|
||
| function makeExecutionContextMock<T extends ExecutionContext>() { | ||
| return { | ||
| waitUntil: vi.fn(), | ||
| passThroughOnException: vi.fn(), | ||
| } as unknown as Mocked<T>; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.