From c0aa90bab853506e769608b4c6c367ea9ade6593 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 22 Sep 2026 16:14:43 +0200 Subject: [PATCH] fix(cloudflare): Bind pass-through Queue producer methods to the binding The Queue producer proxy returned methods other than send and sendBatch unbound, so calling a native method like metrics() ran it with the proxy as `this` and workerd threw "Illegal invocation". Co-Authored-By: Claude Opus 5 --- .../suites/queue/index.ts | 4 ++++ .../suites/queue/test.ts | 16 ++++++++++++++++ .../worker/instrumentQueueProducer.ts | 4 +++- .../worker/instrumentQueueProducer.test.ts | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/queue/index.ts b/dev-packages/cloudflare-integration-tests/suites/queue/index.ts index 405b86695ed4..55968af1f0fa 100644 --- a/dev-packages/cloudflare-integration-tests/suites/queue/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/queue/index.ts @@ -35,6 +35,10 @@ export default Sentry.withSentry( return new Response('enqueued batch'); } + if (url.pathname === '/metrics') { + return Response.json(await env.MY_QUEUE.metrics()); + } + return new Response('not found', { status: 404 }); }, async queue(batch: MessageBatch<{ trigger?: 'error'; payload?: string }>) { diff --git a/dev-packages/cloudflare-integration-tests/suites/queue/test.ts b/dev-packages/cloudflare-integration-tests/suites/queue/test.ts index f0886ba3f37c..6d116ef37bbe 100644 --- a/dev-packages/cloudflare-integration-tests/suites/queue/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/queue/test.ts @@ -126,3 +126,19 @@ it('emits a queue.publish span with batch attributes on env.MY_QUEUE.sendBatch', await runner.makeRequest('post', '/enqueue/batch'); await runner.completed(); }); + +it('calls non-instrumented Queue methods like metrics() on the underlying binding', async ({ signal }) => { + const runner = createRunner(__dirname) + .expect((envelope: Envelope) => { + expect(envelopeItemType(envelope)).toBe('transaction'); + expect(envelopeItem(envelope)).toMatchObject({ + transaction: 'GET /metrics', + contexts: { trace: { status: 'ok' } }, + }); + }) + .start(signal); + + const metrics = await runner.makeRequest('get', '/metrics'); + expect(metrics).toMatchObject({ backlogCount: expect.any(Number), backlogBytes: expect.any(Number) }); + await runner.completed(); +}); diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts index 293b027d5441..d1b3de4ba107 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts @@ -99,7 +99,9 @@ export function instrumentQueueProducer(queue: T, bindingName: }; } - return Reflect.get(target, prop, receiver); + const value = Reflect.get(target, prop) as unknown; + + return typeof value === 'function' ? value.bind(target) : value; }, }); } diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts index 5b291bea7e3e..f176d92e53db 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts @@ -178,4 +178,19 @@ describe('instrumentQueueProducer', () => { const wrapped = instrumentQueueProducer(queue, 'MY_QUEUE') as Queue & { customMethod: () => string }; expect(wrapped.customMethod()).toBe('hi'); }); + + test('calls pass-through methods with the underlying queue as `this`', async () => { + const metrics = { backlogCount: 3, backlogBytes: 42 }; + const queue = Object.assign(createMockQueue(), { + metrics(this: unknown) { + if (this !== queue) { + throw new TypeError('Illegal invocation: function called with incorrect `this` reference.'); + } + return Promise.resolve(metrics); + }, + }) as unknown as Queue & { metrics: () => Promise }; + const wrapped = instrumentQueueProducer(queue, 'MY_QUEUE') as typeof queue; + + await expect(wrapped.metrics()).resolves.toEqual(metrics); + }); });