From a03c6c0cb9763c426430a45fceec6c9ee216243f Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Tue, 26 May 2026 13:33:22 +0800 Subject: [PATCH] fix: resolve writeEarlyHints hang with empty hints on Node.js Closes #1383 Co-authored-by: Cursor --- src/utils/response.ts | 24 +++++++++++++++++++----- test/utils.test.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/utils/response.ts b/src/utils/response.ts index 59510c6cd..a943ac2f5 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -112,26 +112,40 @@ export function writeEarlyHints( event: H3Event, hints: Record, ): void | Promise { + const links = getEarlyHintLinks(hints); + if (links.length === 0) { + return; + } + // Use native early hints if available (Node.js) if (event.runtime?.node?.res?.writeEarlyHints) { return new Promise((resolve) => { - event.runtime?.node?.res?.writeEarlyHints(hints, () => resolve()); + event.runtime?.node?.res?.writeEarlyHints( + { link: links.length === 1 ? links[0]! : links }, + () => resolve(), + ); }); } // Fallback: Set Link headers for CDN support (only Link headers to avoid leaking sensitive headers) + for (const link of links) { + event.res.headers.append("link", link); + } +} + +function getEarlyHintLinks(hints: Record): string[] { + const links: string[] = []; for (const [name, value] of Object.entries(hints)) { if (name.toLowerCase() !== "link") { continue; } if (Array.isArray(value)) { - for (const v of value) { - event.res.headers.append("link", v); - } + links.push(...value); } else { - event.res.headers.append("link", value); + links.push(value); } } + return links; } /** diff --git a/test/utils.test.ts b/test/utils.test.ts index 2a784ebb3..3b85066f0 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -470,6 +470,19 @@ describeMatrix("utils", (t, { it, describe, expect }) => { }); describe("writeEarlyHints", () => { + it.skipIf(t.target !== "node")( + "resolves immediately when hints are empty on Node.js", + async () => { + t.app.get("/", async (event) => { + await writeEarlyHints(event, {}); + return "ok"; + }); + + const res = await t.fetch("/"); + expect(await res.text()).toBe("ok"); + }, + ); + // In Node.js, native writeEarlyHints sends 103 Early Hints status, // so the Link header fallback is not used. Test fallback in web target only. it.skipIf(t.target === "node")(