Skip to content

Commit c6f39f9

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 8d2aa9a commit c6f39f9

3 files changed

Lines changed: 93 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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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("refers to a Durable Object class",
34+
env.lock.getExportedHandler("SomeActor"_kj, kj::none, Frankenvalue{}, kj::none, true));
35+
});
36+
}
37+
38+
KJ_TEST("getExportedHandler: missing entrypoint via dynamic dispatch throws JSG TypeError") {
39+
TestFixture fixture({
40+
.mainModuleSource = R"(
41+
export default { async fetch(req) { return new Response("ok"); } }
42+
)"_kj,
43+
});
44+
45+
fixture.runInIoContext([&](const TestFixture::Environment& env) {
46+
KJ_EXPECT_THROW_MESSAGE("was not found in this worker",
47+
env.lock.getExportedHandler("nonExistent"_kj, kj::none, Frankenvalue{}, kj::none, true));
48+
});
49+
}
50+
51+
// ---------------------------------------------------------------------------
52+
// isDynamicDispatch = false: both error cases must NOT throw a JSG TypeError
53+
// (they log and then throw a non-JSG internal error via KJ_FAIL_ASSERT).
54+
// ---------------------------------------------------------------------------
55+
56+
KJ_TEST("getExportedHandler: DO class via static dispatch throws internal error") {
57+
TestFixture fixture({
58+
.mainModuleSource = R"(
59+
import { DurableObject } from "cloudflare:workers";
60+
export class SomeActor extends DurableObject {}
61+
export default { async fetch(req) { return new Response("ok"); } }
62+
)"_kj,
63+
});
64+
65+
fixture.runInIoContext([&](const TestFixture::Environment& env) {
66+
// LOG_ERROR_PERIODICALLY fires, then KJ_FAIL_ASSERT throws.
67+
// No JSG TypeError — the error is treated as internal.
68+
KJ_EXPECT_LOG(ERROR, "worker has no such named entrypoint");
69+
KJ_EXPECT_THROW_MESSAGE("Unable to get exported handler",
70+
env.lock.getExportedHandler("nonExistent"_kj, kj::none, Frankenvalue{}, kj::none, false));
71+
});
72+
}
73+
74+
} // namespace
75+
} // namespace workerd

src/workerd/io/worker.c++

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2365,7 +2365,16 @@ kj::Maybe<kj::Own<api::ExportedHandler>> Worker::Lock::getExportedHandler(
23652365
return kj::none;
23662366
} else {
23672367
if (worker.impl->actorClasses.find(n) != kj::none) {
2368-
LOG_ERROR_PERIODICALLY("worker is not an actor but class name was requested", n);
2368+
if (isDynamicDispatch) {
2369+
JSG_FAIL_REQUIRE(TypeError, "The entrypoint name ", n,
2370+
" refers to a Durable Object class, but the incoming request is trying to invoke it as"
2371+
" a stateless worker.");
2372+
} else {
2373+
LOG_ERROR_PERIODICALLY("worker is not an actor but class name was requested", n);
2374+
}
2375+
} else if (isDynamicDispatch) {
2376+
JSG_FAIL_REQUIRE(TypeError, "The entrypoint name ", n,
2377+
" was not found in this worker. Ensure the worker exports an entrypoint with that name.");
23692378
} else {
23702379
LOG_ERROR_PERIODICALLY("worker has no such named entrypoint", n);
23712380
}

0 commit comments

Comments
 (0)