Skip to content

Commit 4bcbdc5

Browse files
authored
Output caching headers on RSC/prefetch responses (#2559)
1 parent 5d72b35 commit 4bcbdc5

File tree

7 files changed

+96
-35
lines changed

7 files changed

+96
-35
lines changed

bun.lockb

80 Bytes
Binary file not shown.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
},
99
"packageManager": "bun@1.1.18",
1010
"patchedDependencies": {
11-
"@vercel/next@4.3.15": "patches/@vercel%2Fnext@4.3.15.patch"
11+
"@vercel/next@4.3.15": "patches/@vercel%2Fnext@4.3.15.patch",
12+
"@cloudflare/next-on-pages@1.13.5": "patches/@cloudflare%2Fnext-on-pages@1.13.5.patch"
1213
},
1314
"private": true,
1415
"scripts": {

packages/gitbook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"dev": "env-cmd --silent -f ../../.env.local next dev",
77
"build": "next build",
8-
"build:cloudflare": "next-on-pages",
8+
"build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts",
99
"start": "next start",
1010
"lint": "next lint",
1111
"typecheck": "tsc --noEmit",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @ts-ignore
2+
import nextOnPagesHandler from '@cloudflare/next-on-pages/fetch-handler';
3+
4+
import { withMiddlewareHeadersStorage } from './lib/middleware';
5+
6+
/**
7+
* We use a custom entrypoint until we can move to opennext (https://github.com/opennextjs/opennextjs-cloudflare/issues/92).
8+
* There is a bug in next-on-pages where headers can't be set on the response in the middleware for RSC requests (https://github.com/cloudflare/next-on-pages/issues/897).
9+
*/
10+
export default {
11+
async fetch(request, env, ctx) {
12+
const response = await withMiddlewareHeadersStorage(() =>
13+
nextOnPagesHandler.fetch(request, env, ctx),
14+
);
15+
16+
return response;
17+
},
18+
} as ExportedHandler<{ ASSETS: Fetcher }>;

packages/gitbook/src/lib/middleware.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
import { AsyncLocalStorage } from 'node:async_hooks';
2+
3+
/**
4+
* Set a header on the middleware response.
5+
* We do this because of https://github.com/opennextjs/opennextjs-cloudflare/issues/92
6+
* It can be removed as soon as we move to opennext where hopefully this is fixed.
7+
*/
8+
export function setMiddlewareHeader(response: Response, name: string, value: string) {
9+
const responseHeadersLocalStorage =
10+
// @ts-ignore
11+
globalThis.responseHeadersLocalStorage as AsyncLocalStorage<Headers> | undefined;
12+
const responseHeaders = responseHeadersLocalStorage?.getStore();
13+
response.headers.set(name, value);
14+
15+
if (responseHeaders) {
16+
responseHeaders.set(name, value);
17+
}
18+
}
19+
20+
/**
21+
* Wrap some middleware with a the storage to store headers.
22+
*/
23+
export async function withMiddlewareHeadersStorage(
24+
handler: () => Promise<Response>,
25+
): Promise<Response> {
26+
const responseHeadersLocalStorage =
27+
// @ts-ignore
28+
(globalThis.responseHeadersLocalStorage as AsyncLocalStorage<Headers>) ??
29+
new AsyncLocalStorage<Headers>();
30+
// @ts-ignore
31+
globalThis.responseHeadersLocalStorage = responseHeadersLocalStorage;
32+
33+
const responseHeaders = new Headers();
34+
const response = await responseHeadersLocalStorage.run(responseHeaders, handler);
35+
36+
for (const [name, value] of responseHeaders.entries()) {
37+
response.headers.set(name, value);
38+
}
39+
40+
return response;
41+
}
42+
143
/**
244
* For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content.
345
* The approach is optimized to aim at reusing cached lookup results as much as possible.

packages/gitbook/src/middleware.ts

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import { race } from '@/lib/async';
2121
import { buildVersion } from '@/lib/build';
2222
import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp';
23-
import { getURLLookupAlternatives, normalizeURL } from '@/lib/middleware';
23+
import { getURLLookupAlternatives, normalizeURL, setMiddlewareHeader } from '@/lib/middleware';
2424
import {
2525
VisitorAuthCookieValue,
2626
getVisitorAuthCookieName,
@@ -253,43 +253,31 @@ export async function middleware(request: NextRequest) {
253253
resolved.cookies,
254254
);
255255

256-
response.headers.set('x-gitbook-version', buildVersion());
256+
setMiddlewareHeader(response, 'x-gitbook-version', buildVersion());
257257

258258
// Add Content Security Policy header
259-
response.headers.set('content-security-policy', csp);
259+
setMiddlewareHeader(response, 'content-security-policy', csp);
260260
// Basic security headers
261-
response.headers.set('strict-transport-security', 'max-age=31536000');
262-
response.headers.set('referrer-policy', 'no-referrer-when-downgrade');
263-
response.headers.set('x-content-type-options', 'nosniff');
264-
265-
const isPrefetch = request.headers.has('x-middleware-prefetch');
266-
267-
if (isPrefetch) {
268-
// To avoid cache poisoning, we don't cache prefetch requests
269-
response.headers.set(
270-
'cache-control',
271-
'private, no-cache, no-store, max-age=0, must-revalidate',
272-
);
273-
} else {
274-
if (typeof resolved.cacheMaxAge === 'number') {
275-
const cacheControl = `public, max-age=0, s-maxage=${resolved.cacheMaxAge}, stale-if-error=0`;
276-
277-
if (
278-
process.env.GITBOOK_OUTPUT_CACHE === 'true' &&
279-
process.env.NODE_ENV !== 'development'
280-
) {
281-
response.headers.set('cache-control', cacheControl);
282-
response.headers.set('Cloudflare-CDN-Cache-Control', cacheControl);
283-
} else {
284-
response.headers.set('x-gitbook-cache-control', cacheControl);
285-
}
261+
setMiddlewareHeader(response, 'strict-transport-security', 'max-age=31536000');
262+
setMiddlewareHeader(response, 'referrer-policy', 'no-referrer-when-downgrade');
263+
setMiddlewareHeader(response, 'x-content-type-options', 'nosniff');
264+
265+
if (typeof resolved.cacheMaxAge === 'number') {
266+
const cacheControl = `public, max-age=0, s-maxage=${resolved.cacheMaxAge}, stale-if-error=0`;
267+
268+
if (process.env.GITBOOK_OUTPUT_CACHE === 'true' && process.env.NODE_ENV !== 'development') {
269+
setMiddlewareHeader(response, 'cache-control', cacheControl);
270+
setMiddlewareHeader(response, 'Cloudflare-CDN-Cache-Control', cacheControl);
271+
} else {
272+
setMiddlewareHeader(response, 'x-gitbook-cache-control', cacheControl);
286273
}
274+
}
275+
// }
287276

288-
if (resolved.cacheTags && resolved.cacheTags.length > 0) {
289-
const headerCacheTag = resolved.cacheTags.join(',');
290-
response.headers.set('cache-tag', headerCacheTag);
291-
response.headers.set('x-gitbook-cache-tag', headerCacheTag);
292-
}
277+
if (resolved.cacheTags && resolved.cacheTags.length > 0) {
278+
const headerCacheTag = resolved.cacheTags.join(',');
279+
setMiddlewareHeader(response, 'cache-tag', headerCacheTag);
280+
setMiddlewareHeader(response, 'x-gitbook-cache-tag', headerCacheTag);
293281
}
294282

295283
return response;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
diff --git a/dist/index.js b/dist/index.js
2+
index 32fec63484ec332eb291a7253e5e168223627535..653dee64794140bafe57219356c712b197c17530 100644
3+
--- a/dist/index.js
4+
+++ b/dist/index.js
5+
@@ -6983,6 +6983,7 @@ async function buildWorkerFile({ vercelConfig, vercelOutput }, {
6+
outfile: outputFile,
7+
allowOverwrite: true,
8+
bundle: true,
9+
+ external: ["node:*", "cloudflare:*"],
10+
plugins: [
11+
{
12+
name: "custom-entrypoint-import-plugin",

0 commit comments

Comments
 (0)