Skip to content

Commit 22ea896

Browse files
authored
Merge pull request #22 from netlify/rs/modify-headers
fix: modify response headers before `res.send`
2 parents 7904318 + fdf7c64 commit 22ea896

File tree

5 files changed

+58
-59
lines changed

5 files changed

+58
-59
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
},
3535
"homepage": "https://github.com/netlify/next-runtime-minimal#readme",
3636
"dependencies": {
37-
"@fastly/http-compute-js": "github:orinokai/http-compute-js",
37+
"@fastly/http-compute-js": "1.1.1",
3838
"@netlify/blobs": "^1.6.0",
3939
"@netlify/build": "^29.20.6",
4040
"@netlify/functions": "^2.0.1",

src/handlers/server.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { toComputeResponse, toReqRes } from '@fastly/http-compute-js'
2+
import type { HeadersSentEvent } from '@fastly/http-compute-js/dist/http-compute-js/http-outgoing.js'
23
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
34
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
45

@@ -26,6 +27,14 @@ export default async (request: Request) => {
2627

2728
const { req, res } = toReqRes(request)
2829

30+
res.prependListener('_headersSent', (event: HeadersSentEvent) => {
31+
const headers = new Headers(event.headers)
32+
setCacheControlHeaders(headers)
33+
setVaryHeaders(headers, request, nextConfig)
34+
event.headers = Object.fromEntries(headers.entries())
35+
console.log('Modified response headers:', JSON.stringify(event.headers, null, 2))
36+
})
37+
2938
try {
3039
console.log('Next server request:', req.url)
3140
await nextHandler(req, res)
@@ -39,8 +48,5 @@ export default async (request: Request) => {
3948
const response = { headers: res.getHeaders(), statusCode: res.statusCode }
4049
console.log('Next server response:', JSON.stringify(response, null, 2))
4150

42-
setCacheControlHeaders(res)
43-
setVaryHeaders(res, req, nextConfig)
44-
4551
return toComputeResponse(res)
4652
}

src/helpers/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ export const WORKING_DIR = process.cwd()
88
export const BUILD_DIR = `${WORKING_DIR}/.netlify`
99
export const RUN_DIR = WORKING_DIR
1010

11-
export const SERVER_FUNCTIONS_DIR = `${WORKING_DIR}/.netlify/functions-internal`
11+
export const SERVER_FUNCTIONS_DIR = `${BUILD_DIR}/functions-internal`
1212
export const SERVER_HANDLER_NAME = '___netlify-server-handler'
1313
export const SERVER_HANDLER_DIR = `${SERVER_FUNCTIONS_DIR}/${SERVER_HANDLER_NAME}`
1414

15-
export const EDGE_FUNCTIONS_DIR = `${WORKING_DIR}/.netlify/edge-functions`
15+
export const EDGE_FUNCTIONS_DIR = `${BUILD_DIR}/edge-functions`
1616
export const EDGE_HANDLER_NAME = '___netlify-edge-handler'
1717
export const EDGE_HANDLER_DIR = `${EDGE_FUNCTIONS_DIR}/${EDGE_HANDLER_NAME}`

src/helpers/headers.ts

Lines changed: 39 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,87 @@
1-
import type { IncomingMessage, ServerResponse } from 'http'
2-
31
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
42

5-
type HeaderValue = number | string | string[]
6-
7-
interface NetlifyVaryDirectives {
3+
interface NetlifyVaryValues {
84
headers: string[]
95
languages: string[]
106
cookies: string[]
117
}
128

13-
const generateNetlifyVaryDirectives = ({
14-
headers,
15-
languages,
16-
cookies,
17-
}: NetlifyVaryDirectives): string[] => {
18-
const directives = []
9+
const generateNetlifyVaryValues = ({ headers, languages, cookies }: NetlifyVaryValues): string => {
10+
const values = []
1911
if (headers.length !== 0) {
20-
directives.push(`header=${headers.join(`|`)}`)
12+
values.push(`header=${headers.join(`|`)}`)
2113
}
2214
if (languages.length !== 0) {
23-
directives.push(`language=${languages.join(`|`)}`)
15+
values.push(`language=${languages.join(`|`)}`)
2416
}
2517
if (cookies.length !== 0) {
26-
directives.push(`cookie=${cookies.join(`|`)}`)
18+
values.push(`cookie=${cookies.join(`|`)}`)
2719
}
28-
return directives
20+
return values.join(',')
2921
}
3022

31-
/**
32-
* Parse a header value into an array of directives
33-
*/
34-
const getDirectives = (headerValue: HeaderValue): string[] => {
35-
const directives = Array.isArray(headerValue) ? headerValue : String(headerValue).split(',')
36-
return directives.map((directive) => directive.trim())
23+
const getHeaderValueArray = (header: string): string[] => {
24+
return header.split(',').map((value) => value.trim())
3725
}
26+
27+
const removeHeaderValues = (header: string, values: string[]): string => {
28+
const headerValues = getHeaderValueArray(header)
29+
const filteredValues = headerValues.filter(
30+
(value) => !values.some((val) => value.startsWith(val)),
31+
)
32+
return filteredValues.join(', ')
33+
}
34+
3835
/**
3936
* Ensure the Netlify CDN varies on things that Next.js varies on,
4037
* e.g. i18n, preview mode, etc.
4138
*/
4239
export const setVaryHeaders = (
43-
res: ServerResponse,
44-
req: IncomingMessage,
40+
headers: Headers,
41+
request: Request,
4542
{ basePath, i18n }: NextConfigComplete,
4643
) => {
47-
const netlifyVaryDirectives: NetlifyVaryDirectives = {
44+
const netlifyVaryValues: NetlifyVaryValues = {
4845
headers: [],
4946
languages: [],
5047
cookies: ['__prerender_bypass', '__next_preview_data'],
5148
}
5249

53-
const vary = res.getHeader('vary')
54-
if (vary !== undefined) {
55-
netlifyVaryDirectives.headers.push(...getDirectives(vary))
50+
const vary = headers.get('vary')
51+
if (vary !== null) {
52+
netlifyVaryValues.headers.push(...getHeaderValueArray(vary))
5653
}
5754

58-
const path = new URL(req.url ?? '/', `http://${req.headers.host}`).pathname
55+
const path = new URL(request.url).pathname
5956
const locales = i18n && i18n.localeDetection !== false ? i18n.locales : []
6057

6158
if (locales.length > 1) {
6259
const logicalPath = basePath && path.startsWith(basePath) ? path.slice(basePath.length) : path
6360
if (logicalPath === `/`) {
64-
netlifyVaryDirectives.languages.push(...locales)
65-
netlifyVaryDirectives.cookies.push(`NEXT_LOCALE`)
61+
netlifyVaryValues.languages.push(...locales)
62+
netlifyVaryValues.cookies.push(`NEXT_LOCALE`)
6663
}
6764
}
6865

69-
res.setHeader(`netlify-vary`, generateNetlifyVaryDirectives(netlifyVaryDirectives))
66+
headers.set(`netlify-vary`, generateNetlifyVaryValues(netlifyVaryValues))
7067
}
7168

7269
/**
7370
* Ensure stale-while-revalidate and s-maxage don't leak to the client, but
7471
* assume the user knows what they are doing if CDN cache controls are set
7572
*/
76-
export const setCacheControlHeaders = (res: ServerResponse) => {
77-
const cacheControl = res.getHeader('cache-control')
73+
export const setCacheControlHeaders = (headers: Headers) => {
74+
const cacheControl = headers.get('cache-control')
7875
if (
79-
cacheControl !== undefined &&
80-
!res.hasHeader('cdn-cache-control') &&
81-
!res.hasHeader('netlify-cdn-cache-control')
76+
cacheControl !== null &&
77+
!headers.has('cdn-cache-control') &&
78+
!headers.has('netlify-cdn-cache-control')
8279
) {
83-
const directives = getDirectives(cacheControl).filter(
84-
(directive) =>
85-
!directive.startsWith('s-maxage') && !directive.startsWith('stale-while-revalidate'),
86-
)
87-
88-
res.setHeader('netlify-cdn-cache-control', cacheControl)
89-
res.setHeader(
90-
'cache-control',
91-
directives.length === 0 ? 'public, max-age=0, must-revalidate' : directives,
92-
)
80+
const clientCacheControl = removeHeaderValues(cacheControl, [
81+
's-maxage',
82+
'stale-while-revalidate',
83+
])
84+
headers.set('cache-control', clientCacheControl || 'public, max-age=0, must-revalidate')
85+
headers.set('netlify-cdn-cache-control', cacheControl)
9386
}
9487
}

0 commit comments

Comments
 (0)