Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/2.utils/2.response.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,6 @@ app.get("/", () => {

Write `HTTP/1.1 103 Early Hints` to the client.

In runtimes that don't support early hints natively, this function falls back to setting response headers which can be used by CDN.

<!-- /automd -->
26 changes: 20 additions & 6 deletions src/utils/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,31 @@ export function redirect(

/**
* Write `HTTP/1.1 103 Early Hints` to the client.
*
* In runtimes that don't support early hints natively, this function
* falls back to setting response headers which can be used by CDN.
*/
export function writeEarlyHints(
event: H3Event,
hints: Record<string, string>,
hints: Record<string, string | string[]>,
): void | Promise<void> {
if (!event.runtime?.node?.res?.writeEarlyHints) {
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());
});
}

// Fallback: Set response headers for CDN support
for (const [name, value] of Object.entries(hints)) {
if (Array.isArray(value)) {
for (const v of value) {
event.res.headers.append(name, v);
}
} else {
event.res.headers.append(name, value);
}
}
return new Promise((resolve) => {
event.runtime?.node?.res?.writeEarlyHints(hints, () => resolve());
});
}

/**
Expand Down
31 changes: 31 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getRequestFingerprint,
handleCacheHeaders,
html,
writeEarlyHints,
} from "../src/index.ts";
import { describeMatrix } from "./_setup.ts";

Expand Down Expand Up @@ -304,6 +305,36 @@ describeMatrix("utils", (t, { it, describe, expect }) => {
});
});

describe("writeEarlyHints", () => {
// 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")(
"sets Link header as fallback when native early hints not available",
async () => {
t.app.get("/", async (event) => {
await writeEarlyHints(event, {
Link: "</style.css>; rel=preload; as=style",
});
return "ok";
});
t.app.get("/multi", async (event) => {
await writeEarlyHints(event, {
Link: ["</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"],
});
return "ok";
});

const res = await t.fetch("/");
expect(res.headers.get("Link")).toBe("</style.css>; rel=preload; as=style");

const res2 = await t.fetch("/multi");
expect(res2.headers.get("Link")).toBe(
"</style.css>; rel=preload; as=style, </script.js>; rel=preload; as=script",
);
},
);
});

describe("handleCacheHeaders", () => {
it("can handle cache headers", async () => {
t.app.use((event) => {
Expand Down
Loading