@@ -80,12 +80,34 @@ class TestResponse final: public kj::HttpService::Response {
8080 kj::StringPtr statusText,
8181 const kj::HttpHeaders& headers,
8282 kj::Maybe<uint64_t > expectedBodySize) override {
83+ this ->statusCode = statusCode;
8384 return kj::heap<kj::NullStream>();
8485 }
8586
8687 kj::Own<kj::WebSocket> acceptWebSocket (const kj::HttpHeaders& headers) override {
8788 KJ_FAIL_ASSERT (" request unexpectedly returned a WebSocket" );
8889 }
90+
91+ uint statusCode = 0 ;
92+ };
93+
94+ class RetryClaimObserver final : public RequestObserver {
95+ public:
96+ void claimRetryTokenBeforeUserCode () override {
97+ KJ_EXPECT (stage == 0 );
98+ stage = 1 ;
99+ if (rejectClaim) {
100+ kj::throwFatalException (KJ_EXCEPTION (FAILED , " claim rejected" ));
101+ }
102+ }
103+
104+ void delivered () override {
105+ KJ_EXPECT (stage == 1 );
106+ stage = 2 ;
107+ }
108+
109+ uint stage = 0 ;
110+ bool rejectClaim = false ;
89111};
90112
91113class ThrowingResponse final : public kj::HttpService::Response {
@@ -195,6 +217,71 @@ class RecordingObserver final: public RequestObserver, public WorkerInterface {
195217 kj::Maybe<WorkerInterface&> inner;
196218};
197219
220+ KJ_TEST (" retry claim fires synchronously before fetch delivery" ) {
221+ auto observer = kj::refcounted<RetryClaimObserver>();
222+ TestFixture fixture (TestFixture::SetupParams{
223+ .mainModuleSource = R"SCRIPT(
224+ export default {
225+ async fetch() {
226+ return new Response("OK");
227+ },
228+ };
229+ )SCRIPT" _kj,
230+ .requestObserverFactory = kj::Function<kj::Own<RequestObserver>()>(
231+ [&observer]() -> kj::Own<RequestObserver> { return kj::addRef (*observer); }),
232+ });
233+
234+ auto entrypoint = fixture.makeWorkerEntrypoint ();
235+ kj::HttpHeaderTable headerTable;
236+ kj::HttpHeaders headers (headerTable);
237+ kj::NullStream requestBody;
238+ TestResponse response;
239+
240+ auto request = entrypoint->request (
241+ kj::HttpMethod::GET , " https://example.com" , headers, requestBody, response);
242+ KJ_EXPECT (observer->stage == 2 , " claim and delivery hooks did not fire synchronously" );
243+ request.wait (fixture.getWaitScope ());
244+
245+ KJ_EXPECT (response.statusCode == 200 );
246+ KJ_EXPECT (observer->stage == 2 , " claim hook fired more than once" );
247+ }
248+
249+ KJ_TEST (" rejected retry claim prevents actor construction and delivery" ) {
250+ auto observer = kj::refcounted<RetryClaimObserver>();
251+ observer->rejectClaim = true ;
252+ TestFixture fixture (TestFixture::SetupParams{
253+ .mainModuleSource = R"SCRIPT(
254+ export default class {
255+ constructor() {
256+ throw new Error("actor was constructed");
257+ }
258+
259+ async fetch() {
260+ return new Response("unexpected");
261+ }
262+ }
263+ )SCRIPT" _kj,
264+ .actorId = Worker::Actor::Id (kj::str (" retry-claim-test" )),
265+ .requestObserverFactory = kj::Function<kj::Own<RequestObserver>()>(
266+ [&observer]() -> kj::Own<RequestObserver> { return kj::addRef (*observer); }),
267+ });
268+
269+ auto entrypoint = fixture.makeWorkerEntrypoint ();
270+ kj::HttpHeaderTable headerTable;
271+ kj::HttpHeaders headers (headerTable);
272+ kj::NullStream requestBody;
273+ TestResponse response;
274+
275+ auto exception = kj::runCatchingExceptions ([&]() {
276+ entrypoint->request (kj::HttpMethod::GET , " https://example.com" , headers, requestBody, response)
277+ .wait (fixture.getWaitScope ());
278+ });
279+
280+ KJ_EXPECT (KJ_ASSERT_NONNULL (exception).getDescription ().contains (" claim rejected" ));
281+ KJ_EXPECT (observer->stage == 1 , " request was delivered after its retry claim was rejected" );
282+ KJ_EXPECT (response.statusCode == 0 );
283+ }
284+
198285KJ_TEST (" connect pass-through tags failures after delivery" ) {
199286 capnp::MallocMessageBuilder flagsMessage;
200287 auto flags = flagsMessage.initRoot <CompatibilityFlags>();
0 commit comments