From 5c8b5d53ba5d568c52834e41659b32cddadc952f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Mon, 23 Feb 2026 15:15:34 -0800 Subject: [PATCH] fix(early-hints): early hints don't need to be sync inside a FetchEvent handler Fixes: https://github.com/fastly/js-compute-runtime/issues/1322 --- .../fixtures/app/src/early-hints.js | 61 +++++++++++++++++++ .../js-compute/fixtures/app/tests.json | 54 ++++++++++++++++ runtime/fastly/builtins/fetch-event.cpp | 5 -- 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/integration-tests/js-compute/fixtures/app/src/early-hints.js b/integration-tests/js-compute/fixtures/app/src/early-hints.js index 817de3264f..2e8b702a9c 100644 --- a/integration-tests/js-compute/fixtures/app/src/early-hints.js +++ b/integration-tests/js-compute/fixtures/app/src/early-hints.js @@ -11,11 +11,56 @@ routes.set('/early-hints/manual-response', (event) => { event.respondWith(new Response('ok')); }); +routes.set('/early-hints/manual-response-late', (event) => { + event.respondWith(new Response('ok')); + assertThrows(() => { + event.respondWith( + new Response(null, { + status: 103, + headers: { link: '; rel=preload; as=style' }, + }), + ); + }); +}); + +routes.set('/early-hints/manual-response-async', async (event) => { + await (async () => { + event.respondWith( + new Response(null, { + status: 103, + headers: { link: '; rel=preload; as=style' }, + }), + ); + event.respondWith(new Response('ok')); + })(); +}); + +routes.set('/early-hints/manual-response-late-async', async (event) => { + await (async () => { + event.respondWith(new Response('ok')); + assertThrows(() => { + event.respondWith( + new Response(null, { + status: 103, + headers: { link: '; rel=preload; as=style' }, + }), + ); + }); + })(); +}); + routes.set('/early-hints/send-early-hints', (event) => { event.sendEarlyHints({ link: '; rel=preload; as=style' }); event.respondWith(new Response('ok')); }); +routes.set('/early-hints/send-early-hints-late', (event) => { + event.respondWith(new Response('ok')); + assertThrows(() => { + event.sendEarlyHints({ link: '; rel=preload; as=style' }); + }); +}); + routes.set('/early-hints/send-early-hints-multiple-headers', (event) => { event.sendEarlyHints([ ['link', '; rel=preload; as=style'], @@ -23,3 +68,19 @@ routes.set('/early-hints/send-early-hints-multiple-headers', (event) => { ]); event.respondWith(new Response('ok')); }); + +routes.set('/early-hints/send-early-hints-async', async (event) => { + await (async () => { + event.sendEarlyHints({ link: '; rel=preload; as=style' }); + event.respondWith(new Response('ok')); + })(); +}); + +routes.set('/early-hints/send-early-hints-late-async', async (event) => { + await (async () => { + event.respondWith(new Response('ok')); + assertThrows(() => { + event.sendEarlyHints({ link: '; rel=preload; as=style' }); + }); + })(); +}); diff --git a/integration-tests/js-compute/fixtures/app/tests.json b/integration-tests/js-compute/fixtures/app/tests.json index aa71d1c882..f6161f41d4 100644 --- a/integration-tests/js-compute/fixtures/app/tests.json +++ b/integration-tests/js-compute/fixtures/app/tests.json @@ -3142,6 +3142,33 @@ } } }, + "GET /early-hints/manual-response-late": { + "environments": ["compute"], + "downstream_response": { + "body": "ok", + "status": 200 + } + }, + "GET /early-hints/manual-response-async": { + "environments": ["compute"], + "downstream_response": { + "body": "ok", + "status": 200 + }, + "downstream_info": { + "status": 103, + "headers": { + "link": "; rel=preload; as=style" + } + } + }, + "GET /early-hints/manual-response-late-async": { + "environments": ["compute"], + "downstream_response": { + "body": "ok", + "status": 200 + } + }, "GET /early-hints/send-early-hints": { "environments": ["compute"], "downstream_response": { @@ -3155,6 +3182,13 @@ } } }, + "GET /early-hints/send-early-hints-late": { + "environments": ["compute"], + "downstream_response": { + "body": "ok", + "status": 200 + } + }, "GET /early-hints/send-early-hints-multiple-headers": { "environments": ["compute"], "downstream_response": { @@ -3169,6 +3203,26 @@ ] } }, + "GET /early-hints/send-early-hints-async": { + "environments": ["compute"], + "downstream_response": { + "body": "ok", + "status": 200 + }, + "downstream_info": { + "status": 103, + "headers": { + "link": "; rel=preload; as=style" + } + } + }, + "GET /early-hints/send-early-hints-late-async": { + "environments": ["compute"], + "downstream_response": { + "body": "ok", + "status": 200 + } + }, "GET /shielding/invalid-shield": { "environments": ["compute"] }, diff --git a/runtime/fastly/builtins/fetch-event.cpp b/runtime/fastly/builtins/fetch-event.cpp index e130df8824..efc5181ef1 100644 --- a/runtime/fastly/builtins/fetch-event.cpp +++ b/runtime/fastly/builtins/fetch-event.cpp @@ -908,11 +908,6 @@ bool FetchEvent::sendEarlyHints(JSContext *cx, unsigned argc, JS::Value *vp) { METHOD_HEADER(1) MOZ_RELEASE_ASSERT(state(self) == State::unhandled || state(self) == State::waitToRespond); - if (!is_dispatching(self)) { - JS_ReportErrorUTF8(cx, "FetchEvent#sendEarlyHints must be called synchronously from " - "within a FetchEvent handler"); - return false; - } if (state(self) != State::unhandled && state(self) != State::waitToRespond) { JS_ReportErrorUTF8( cx, "FetchEvent#sendEarlyHints can't be called after the main response has been sent");