Skip to content

Commit df79103

Browse files
committed
Reclassify entrypoint-not-found errors as JSG errors for dynamic dispatch
When isDynamicDispatch is true (entrypoint name supplied at request time), two errors in getExportedHandler() are reclassified as JSG TypeErrors so they surface to the caller rather than being logged as internal errors. This will inform users about their likely misconfiguration: - A DO class name requested via the non-actor dispatch path - A non-existent entrypoint name For static dispatch (isDynamicDispatch false) both cases retain their existing LOG_ERROR_PERIODICALLY behaviour, since they can reflect genuine bugs or infrastructure failures.
1 parent 98fd6db commit df79103

3 files changed

Lines changed: 113 additions & 1 deletion

File tree

src/workerd/io/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,3 +526,11 @@ kj_test(
526526
"//src/workerd/tests:test-fixture",
527527
],
528528
)
529+
530+
kj_test(
531+
src = "worker-getexportedhandler-test.c++",
532+
deps = [
533+
":io",
534+
"//src/workerd/tests:test-fixture",
535+
],
536+
)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) 2024 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
5+
// Tests for Worker::Lock::getExportedHandler() error behaviour, specifically
6+
// the isDynamicDispatch path which surfaces user-configuration mistakes as JSG
7+
// TypeErrors rather than internal log-only errors.
8+
9+
#include <workerd/api/global-scope.h>
10+
#include <workerd/io/frankenvalue.h>
11+
#include <workerd/io/worker.h>
12+
#include <workerd/tests/test-fixture.h>
13+
14+
#include <kj/test.h>
15+
16+
namespace workerd {
17+
namespace {
18+
19+
// ---------------------------------------------------------------------------
20+
// isDynamicDispatch = true: both error cases must throw a JSG TypeError.
21+
// ---------------------------------------------------------------------------
22+
23+
KJ_TEST("getExportedHandler: DO class via dynamic dispatch throws JSG TypeError") {
24+
TestFixture fixture({
25+
.mainModuleSource = R"(
26+
import { DurableObject } from "cloudflare:workers";
27+
export class SomeActor extends DurableObject {}
28+
export default { async fetch(req) { return new Response("ok"); } }
29+
)"_kj,
30+
});
31+
32+
fixture.runInIoContext([&](const TestFixture::Environment& env) {
33+
KJ_EXPECT_THROW_MESSAGE(
34+
"jsg.TypeError: The entrypoint name SomeActor refers to a Durable Object class, but the "
35+
"incoming request is trying to invoke it as a stateless worker.",
36+
env.lock.getExportedHandler("SomeActor"_kj, kj::none, Frankenvalue{}, kj::none, true));
37+
});
38+
}
39+
40+
KJ_TEST("getExportedHandler: missing entrypoint via dynamic dispatch throws JSG TypeError") {
41+
TestFixture fixture({
42+
.mainModuleSource = R"(
43+
export default { async fetch(req) { return new Response("ok"); } }
44+
)"_kj,
45+
});
46+
47+
fixture.runInIoContext([&](const TestFixture::Environment& env) {
48+
KJ_EXPECT_THROW_MESSAGE(
49+
"jsg.TypeError: The entrypoint name nonExistent was not found in this worker. Ensure the "
50+
"worker exports an entrypoint with that name.",
51+
env.lock.getExportedHandler("nonExistent"_kj, kj::none, Frankenvalue{}, kj::none, true));
52+
});
53+
}
54+
55+
// ---------------------------------------------------------------------------
56+
// isDynamicDispatch = false: both error cases must NOT throw a JSG TypeError
57+
// (they log and then throw a non-JSG internal error via KJ_FAIL_ASSERT).
58+
// ---------------------------------------------------------------------------
59+
60+
KJ_TEST("getExportedHandler: DO class via static dispatch throws internal error") {
61+
TestFixture fixture({
62+
.mainModuleSource = R"(
63+
import { DurableObject } from "cloudflare:workers";
64+
export class SomeActor extends DurableObject {}
65+
export default { async fetch(req) { return new Response("ok"); } }
66+
)"_kj,
67+
});
68+
69+
fixture.runInIoContext([&](const TestFixture::Environment& env) {
70+
// LOG_ERROR_PERIODICALLY fires, then KJ_FAIL_ASSERT throws.
71+
// No JSG TypeError — the error is treated as internal.
72+
KJ_EXPECT_LOG(ERROR, "worker is not an actor but class name was requested; n = SomeActor");
73+
KJ_EXPECT_THROW_MESSAGE("worker_do_not_log; Unable to get exported handler",
74+
env.lock.getExportedHandler("SomeActor"_kj, kj::none, Frankenvalue{}, kj::none, false));
75+
});
76+
}
77+
78+
KJ_TEST("getExportedHandler: missing entrypoint via static dispatch throws internal error") {
79+
TestFixture fixture({
80+
.mainModuleSource = R"(
81+
export default { async fetch(req) { return new Response("ok"); } }
82+
)"_kj,
83+
});
84+
85+
fixture.runInIoContext([&](const TestFixture::Environment& env) {
86+
// LOG_ERROR_PERIODICALLY fires, then KJ_FAIL_ASSERT throws.
87+
// No JSG TypeError — the error is treated as internal.
88+
KJ_EXPECT_LOG(ERROR, "worker has no such named entrypoint; n = nonExistent");
89+
KJ_EXPECT_THROW_MESSAGE("worker_do_not_log; Unable to get exported handler",
90+
env.lock.getExportedHandler("nonExistent"_kj, kj::none, Frankenvalue{}, kj::none, false));
91+
});
92+
}
93+
94+
} // namespace
95+
} // namespace workerd

src/workerd/io/worker.c++

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2371,7 +2371,16 @@ kj::Maybe<kj::Own<api::ExportedHandler>> Worker::Lock::getExportedHandler(
23712371
return kj::none;
23722372
} else {
23732373
if (worker.impl->actorClasses.find(n) != kj::none) {
2374-
LOG_ERROR_PERIODICALLY("worker is not an actor but class name was requested", n);
2374+
if (isDynamicDispatch) {
2375+
JSG_FAIL_REQUIRE(TypeError, "The entrypoint name ", n,
2376+
" refers to a Durable Object class, but the incoming request is trying to invoke it as"
2377+
" a stateless worker.");
2378+
} else {
2379+
LOG_ERROR_PERIODICALLY("worker is not an actor but class name was requested", n);
2380+
}
2381+
} else if (isDynamicDispatch) {
2382+
JSG_FAIL_REQUIRE(TypeError, "The entrypoint name ", n,
2383+
" was not found in this worker. Ensure the worker exports an entrypoint with that name.");
23752384
} else {
23762385
LOG_ERROR_PERIODICALLY("worker has no such named entrypoint", n);
23772386
}

0 commit comments

Comments
 (0)