Repository navigation
Fix tracing for thrown JSRPC method errors #7644
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
e54694e
21da486
a5c30ee
dab51a0
19a8074
10203af
add082c
743f803
eda3938
a0ce3f6
9ebfbb4
f456357
60350ff
731a02d
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 |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| // Copyright (c) 2026 Cloudflare, Inc. | ||
| // Licensed under the Apache 2.0 license found in the LICENSE file or at: | ||
| // https://opensource.org/licenses/Apache-2.0 | ||
|
|
||
| import * as assert from 'node:assert'; | ||
|
|
||
| const invocations = []; | ||
| const HUNG_REQUEST_MESSAGE = | ||
| "The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. Refer to: https://developers.cloudflare.com/workers/observability/errors/"; | ||
|
|
||
| export default { | ||
| tailStream(onset) { | ||
| if ( | ||
| onset.event.info?.type !== 'jsrpc' || | ||
| onset.event.entrypoint !== 'ThrowingService' | ||
| ) { | ||
| return () => {}; | ||
| } | ||
|
|
||
| const invocation = { | ||
| methods: [], | ||
| logs: [], | ||
| exceptions: [], | ||
| outcomes: [], | ||
| }; | ||
| invocations.push(invocation); | ||
|
|
||
| return (event) => { | ||
| const item = event.event; | ||
| if (item.type === 'attributes') { | ||
| const method = item.info.find(({ name }) => name === 'jsrpc.method'); | ||
| const targetKind = item.info.find( | ||
| ({ name }) => name === 'jsrpc.target_kind' | ||
| ); | ||
| const operation = item.info.find( | ||
| ({ name }) => name === 'jsrpc.operation' | ||
| ); | ||
| if ( | ||
| method?.value && | ||
| targetKind?.value === 'entrypoint' && | ||
| operation?.value === 'call' | ||
| ) { | ||
| invocation.methods.push(method.value); | ||
| } | ||
| } else if (item.type === 'log') { | ||
| invocation.logs.push(item.message); | ||
| } else if (item.type === 'exception') { | ||
| invocation.exceptions.push({ name: item.name, message: item.message }); | ||
| } else if (item.type === 'outcome') { | ||
| invocation.outcomes.push(item.outcome); | ||
| } | ||
| }; | ||
| }, | ||
| }; | ||
|
|
||
| function findInvocation(method) { | ||
| return invocations.find((invocation) => invocation.methods.includes(method)); | ||
| } | ||
|
|
||
| function hasException(invocation, message) { | ||
| return invocation?.exceptions.some( | ||
| (exception) => exception.message === message | ||
| ); | ||
| } | ||
|
|
||
| function foundExpectedEvents(throwing, asyncThrowing) { | ||
| return ( | ||
| hasException(throwing, 'intentional JSRPC failure') && | ||
| throwing?.outcomes.includes('exception') && | ||
| hasException(asyncThrowing, 'intentional async JSRPC failure') && | ||
| asyncThrowing?.outcomes.includes('exception') | ||
| ); | ||
| } | ||
|
|
||
| export const test = { | ||
| async test() { | ||
| const deadline = Date.now() + 5000; | ||
| let throwing = findInvocation('throwError'); | ||
| let asyncThrowing = findInvocation('throwAsyncError'); | ||
|
|
||
| while ( | ||
| !foundExpectedEvents(throwing, asyncThrowing) && | ||
| Date.now() < deadline | ||
| ) { | ||
| await scheduler.wait(10); | ||
| throwing = findInvocation('throwError'); | ||
| asyncThrowing = findInvocation('throwAsyncError'); | ||
| } | ||
|
|
||
| assert.ok(throwing, 'Could not find throwError JSRPC invocation'); | ||
| assert.ok(asyncThrowing, 'Could not find throwAsyncError JSRPC invocation'); | ||
| assert.strictEqual( | ||
| invocations.length, | ||
| 2, | ||
| 'Expected exactly two callee invocations' | ||
| ); | ||
|
|
||
| assert.deepStrictEqual(throwing.methods, ['throwError']); | ||
| const intentionalFailure = { | ||
| name: 'Error', | ||
| message: 'intentional JSRPC failure', | ||
| }; | ||
| // Invocation cleanup may also emit its cancellation exception, depending on scheduling. | ||
| const expectedThrowingExceptions = [ | ||
| intentionalFailure, | ||
| ...(throwing.exceptions.length === 1 | ||
| ? [] | ||
| : [ | ||
| { | ||
| name: 'Error', | ||
| message: HUNG_REQUEST_MESSAGE, | ||
|
Contributor
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. This is the bug we want to get rid of and this asserts that it's expected behaviour (...sometimes). I'm not at all convinced this is directionally the right fix. I think we need to add a |
||
| }, | ||
| ]), | ||
| ]; | ||
| assert.deepStrictEqual(throwing.exceptions, expectedThrowingExceptions); | ||
| assert.deepStrictEqual(throwing.outcomes, ['exception']); | ||
|
|
||
| assert.deepStrictEqual(asyncThrowing.methods, ['throwAsyncError']); | ||
| assert.deepStrictEqual(asyncThrowing.exceptions, [ | ||
| { | ||
| name: 'Error', | ||
| message: 'intentional async JSRPC failure', | ||
| }, | ||
| ]); | ||
| assert.deepStrictEqual(asyncThrowing.outcomes, ['exception']); | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // Copyright (c) 2026 Cloudflare, Inc. | ||
| // Licensed under the Apache 2.0 license found in the LICENSE file or at: | ||
| // https://opensource.org/licenses/Apache-2.0 | ||
|
|
||
| import { strictEqual } from 'node:assert'; | ||
| import { WorkerEntrypoint } from 'cloudflare:workers'; | ||
|
|
||
| export class ThrowingService extends WorkerEntrypoint { | ||
| throwError() { | ||
| console.log('callee throwError called'); | ||
| throw new Error('intentional JSRPC failure'); | ||
| } | ||
|
|
||
| async throwAsyncError() { | ||
| await scheduler.wait(1); | ||
| throw new Error('intentional async JSRPC failure'); | ||
| } | ||
| } | ||
|
|
||
| export default { | ||
| async test(_controller, env, _ctx) { | ||
| try { | ||
| await env.ThrowingService.throwError(); | ||
| throw new Error('Expected throwError() to reject'); | ||
| } catch (error) { | ||
| strictEqual(error.message, 'intentional JSRPC failure'); | ||
| } | ||
|
|
||
| try { | ||
| await env.ThrowingService.throwAsyncError(); | ||
| throw new Error('Expected throwAsyncError() to reject'); | ||
| } catch (error) { | ||
| strictEqual(error.message, 'intentional async JSRPC failure'); | ||
| } | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| using Workerd = import "/workerd/workerd.capnp"; | ||
|
|
||
| # Test JSRPC callee-side tracing for delivered method failures. | ||
|
|
||
| const unitTests :Workerd.Config = ( | ||
| services = [ | ||
| (name = "jsrpc-error-trace-test", worker = .mainWorker), | ||
| (name = "tail", worker = .tailWorker), | ||
| ], | ||
| ); | ||
|
|
||
| const mainWorker :Workerd.Worker = ( | ||
| modules = [ | ||
| (name = "worker", esModule = embed "jsrpc-error-trace-test.js") | ||
| ], | ||
| compatibilityFlags = ["nodejs_compat", "experimental", "rpc", "enhanced_error_serialization"], | ||
| bindings = [ | ||
| (name = "ThrowingService", service = (name = "jsrpc-error-trace-test", entrypoint = "ThrowingService")), | ||
| ], | ||
| streamingTails = ["tail"], | ||
| ); | ||
|
|
||
| const tailWorker :Workerd.Worker = ( | ||
| modules = [ | ||
| (name = "worker", esModule = embed "jsrpc-error-trace-test-tail.js") | ||
| ], | ||
| compatibilityFlags = ["experimental", "nodejs_compat"], | ||
| ); |
Uh oh!
There was an error while loading. Please reload this page.