Skip to content

Commit 22c2412

Browse files
kathmbeckpieh
andauthored
fix(gatsby-adapter-netlify): adapter use headerRoutes (#38652)
* simplified header rules * lint * lint * update test/snapshot * update snapshot * add snapshot for headerRoutes * adapter use headerRoutes * export type * first pass at headers tests * merge conflict fix * lint error * remove accidental nesting * tests * tests * static assets todo * example of permanent caching header assertion * ensure getServerData header has priority over config header for SSR pages * normalize header values before assertions * add page- and app-data header checks * tmp: skip deleting deploys for easier iteration * refactor test a bit so it's easier to assert same thing in multiple tests + add assertions for js assets * add slice-data headers check * add static query result header test --------- Co-authored-by: Michal Piechowiak <[email protected]>
1 parent 9a26700 commit 22c2412

File tree

9 files changed

+294
-64
lines changed

9 files changed

+294
-64
lines changed

e2e-tests/adapters/cypress/e2e/basics.cy.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
import { title } from "../../constants"
2+
import { WorkaroundCachedResponse } from "../utils/dont-cache-responses-in-browser"
23

34
describe("Basics", () => {
45
beforeEach(() => {
56
cy.intercept("/gatsby-icon.png").as("static-folder-image")
6-
cy.intercept("/static/astro-**.png", req => {
7-
req.on("before:response", res => {
8-
// this generally should be permamently cached, but that cause problems with intercepting
9-
// see https://docs.cypress.io/api/commands/intercept#cyintercept-and-request-caching
10-
// so we disable caching for this response
11-
// tests for cache-control headers should be done elsewhere
12-
13-
res.headers["cache-control"] = "no-store"
14-
})
15-
}).as("img-import")
7+
cy.intercept("/static/astro-**.png", WorkaroundCachedResponse).as(
8+
"img-import"
9+
)
1610

1711
cy.visit("/").waitForRouteChange()
1812
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { WorkaroundCachedResponse } from "../utils/dont-cache-responses-in-browser"
2+
3+
describe("Headers", () => {
4+
const defaultHeaders = {
5+
"x-xss-protection": "1; mode=block",
6+
"x-content-type-options": "nosniff",
7+
"referrer-policy": "same-origin",
8+
"x-frame-options": "DENY",
9+
}
10+
11+
// DRY for repeated assertions in multple tests
12+
const expectedHeadersByRouteAlias = {
13+
"@app-data": {
14+
...defaultHeaders,
15+
"cache-control": "public,max-age=0,must-revalidate",
16+
},
17+
"@page-data": {
18+
...defaultHeaders,
19+
"cache-control": "public,max-age=0,must-revalidate",
20+
},
21+
"@slice-data": {
22+
...defaultHeaders,
23+
"cache-control": "public,max-age=0,must-revalidate",
24+
},
25+
"@static-query-result": {
26+
...defaultHeaders,
27+
"cache-control": "public,max-age=0,must-revalidate",
28+
},
29+
"@img-webpack-import": {
30+
...defaultHeaders,
31+
"cache-control": "public,max-age=31536000,immutable",
32+
},
33+
"@js": {
34+
...defaultHeaders,
35+
"cache-control": "public,max-age=31536000,immutable",
36+
},
37+
}
38+
39+
// `ntl serve` and actual deploy seem to have possible slight differences around header value formatting
40+
// so this just remove spaces around commas to make it easier to compare
41+
function normalizeHeaderValue(value: string | undefined): string | undefined {
42+
if (typeof value === "undefined") {
43+
return value
44+
}
45+
// Remove spaces around commas
46+
return value.replace(/\s*,\s*/gm, `,`)
47+
}
48+
function checkHeaders(
49+
routeAlias: string,
50+
expectedHeaders?: Record<string, string>
51+
) {
52+
if (!expectedHeaders) {
53+
expectedHeaders = expectedHeadersByRouteAlias[routeAlias]
54+
}
55+
56+
if (!expectedHeaders) {
57+
throw new Error(`No expected headers provided for "${routeAlias}`)
58+
}
59+
60+
cy.wait(routeAlias).then(interception => {
61+
Object.keys(expectedHeaders).forEach(headerKey => {
62+
const headers = interception.response.headers[headerKey]
63+
64+
const firstHeader: string = Array.isArray(headers)
65+
? headers[0]
66+
: headers
67+
68+
expect(normalizeHeaderValue(firstHeader)).to.eq(
69+
normalizeHeaderValue(expectedHeaders[headerKey])
70+
)
71+
})
72+
})
73+
}
74+
75+
beforeEach(() => {
76+
cy.intercept("/", WorkaroundCachedResponse).as("index")
77+
cy.intercept("routes/ssr/static", WorkaroundCachedResponse).as("ssr")
78+
cy.intercept("routes/dsg/static", WorkaroundCachedResponse).as("dsg")
79+
80+
cy.intercept("**/page-data.json", WorkaroundCachedResponse).as("page-data")
81+
cy.intercept("**/app-data.json", WorkaroundCachedResponse).as("app-data")
82+
cy.intercept("**/slice-data/*.json", WorkaroundCachedResponse).as(
83+
"slice-data"
84+
)
85+
cy.intercept("**/page-data/sq/d/*.json", WorkaroundCachedResponse).as(
86+
"static-query-result"
87+
)
88+
89+
cy.intercept("/static/astro-**.png", WorkaroundCachedResponse).as(
90+
"img-webpack-import"
91+
)
92+
cy.intercept("*.js", WorkaroundCachedResponse).as("js")
93+
})
94+
95+
it("should contain correct headers for index page", () => {
96+
cy.visit("/").waitForRouteChange()
97+
98+
checkHeaders("@index", {
99+
...defaultHeaders,
100+
"x-custom-header": "my custom header value",
101+
"cache-control": "public,max-age=0,must-revalidate",
102+
})
103+
104+
checkHeaders("@app-data")
105+
checkHeaders("@page-data")
106+
checkHeaders("@slice-data")
107+
checkHeaders("@static-query-result")
108+
109+
// index page is only one showing webpack imported image
110+
checkHeaders("@img-webpack-import")
111+
checkHeaders("@js")
112+
})
113+
114+
it("should contain correct headers for ssr page", () => {
115+
cy.visit("routes/ssr/static").waitForRouteChange()
116+
117+
checkHeaders("@ssr", {
118+
...defaultHeaders,
119+
"x-custom-header": "my custom header value",
120+
"x-ssr-header": "my custom header value from config",
121+
"x-ssr-header-getserverdata": "my custom header value from getServerData",
122+
"x-ssr-header-overwrite": "getServerData wins",
123+
})
124+
125+
checkHeaders("@app-data")
126+
// page-data is baked into SSR page so it's not fetched and we don't assert it
127+
checkHeaders("@slice-data")
128+
checkHeaders("@static-query-result")
129+
checkHeaders("@js")
130+
})
131+
132+
it("should contain correct headers for dsg page", () => {
133+
cy.visit("routes/dsg/static").waitForRouteChange()
134+
135+
checkHeaders("@dsg", {
136+
...defaultHeaders,
137+
"x-custom-header": "my custom header value",
138+
"x-dsg-header": "my custom header value",
139+
})
140+
141+
checkHeaders("@app-data")
142+
checkHeaders("@page-data")
143+
checkHeaders("@slice-data")
144+
checkHeaders("@static-query-result")
145+
checkHeaders("@js")
146+
})
147+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { CyHttpMessages } from "cypress/types/net-stubbing"
2+
3+
/**
4+
* https://docs.cypress.io/api/commands/intercept#cyintercept-and-request-caching
5+
*
6+
* For responses that are to be cached we need to use a trick so browser doesn't cache them
7+
* So this enforces `no-store` cache-control header before response hits the browser
8+
* and then restore original cache-control value for assertions.
9+
*/
10+
export const WorkaroundCachedResponse = (
11+
req: CyHttpMessages.IncomingHttpRequest
12+
): void | Promise<void> => {
13+
req.on("before:response", res => {
14+
res.headers["x-original-cache-control"] = res.headers["cache-control"]
15+
res.headers["cache-control"] = "no-store"
16+
})
17+
req.on("after:response", res => {
18+
res.headers["cache-control"] = res.headers["x-original-cache-control"]
19+
delete res.headers["x-original-cache-control"]
20+
})
21+
}

e2e-tests/adapters/debug-adapter.ts

+21-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { inspect } from "util"
22
import type { AdapterInit } from "gatsby"
33

4-
const createTestingAdapter: AdapterInit = (adapterOptions) => {
4+
const createTestingAdapter: AdapterInit = adapterOptions => {
55
return {
66
name: `gatsby-adapter-debug`,
77
cache: {
@@ -10,28 +10,36 @@ const createTestingAdapter: AdapterInit = (adapterOptions) => {
1010
},
1111
store({ directories, reporter }) {
1212
reporter.info(`[gatsby-adapter-debug] cache.store() ${directories}`)
13-
}
13+
},
1414
},
1515
adapt({
1616
routesManifest,
17+
headerRoutes,
1718
functionsManifest,
1819
pathPrefix,
1920
trailingSlash,
2021
reporter,
2122
}) {
2223
reporter.info(`[gatsby-adapter-debug] adapt()`)
2324

24-
console.log(`[gatsby-adapter-debug] adapt()`, inspect({
25-
routesManifest,
26-
functionsManifest,
27-
pathPrefix,
28-
trailingSlash,
29-
}, {
30-
depth: Infinity,
31-
colors: true
32-
}))
33-
}
25+
console.log(
26+
`[gatsby-adapter-debug] adapt()`,
27+
inspect(
28+
{
29+
routesManifest,
30+
headerRoutes,
31+
functionsManifest,
32+
pathPrefix,
33+
trailingSlash,
34+
},
35+
{
36+
depth: Infinity,
37+
colors: true,
38+
}
39+
)
40+
)
41+
},
3442
}
3543
}
3644

37-
export default createTestingAdapter
45+
export default createTestingAdapter

e2e-tests/adapters/gatsby-config.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import debugAdapter from "./debug-adapter"
33
import { siteDescription, title } from "./constants"
44

55
const shouldUseDebugAdapter = process.env.USE_DEBUG_ADAPTER ?? false
6-
const trailingSlash = (process.env.TRAILING_SLASH || `never`) as GatsbyConfig["trailingSlash"]
6+
const trailingSlash = (process.env.TRAILING_SLASH ||
7+
`never`) as GatsbyConfig["trailingSlash"]
78

89
let configOverrides: GatsbyConfig = {}
910

@@ -21,6 +22,39 @@ const config: GatsbyConfig = {
2122
},
2223
trailingSlash,
2324
plugins: [],
25+
headers: [
26+
{
27+
source: `/*`,
28+
headers: [
29+
{
30+
key: "x-custom-header",
31+
value: "my custom header value",
32+
},
33+
],
34+
},
35+
{
36+
source: `routes/ssr/*`,
37+
headers: [
38+
{
39+
key: "x-ssr-header",
40+
value: "my custom header value from config",
41+
},
42+
{
43+
key: "x-ssr-header-overwrite",
44+
value: "config wins",
45+
},
46+
],
47+
},
48+
{
49+
source: `routes/dsg/*`,
50+
headers: [
51+
{
52+
key: "x-dsg-header",
53+
value: "my custom header value",
54+
},
55+
],
56+
},
57+
],
2458
...configOverrides,
2559
}
2660

e2e-tests/adapters/scripts/deploy-and-run/netlify.mjs

+17-20
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,21 @@ console.log(`Deployed to ${deployInfo.deploy_url}`)
3737
try {
3838
await execa(`npm`, [`run`, npmScriptToRun], { stdio: `inherit` })
3939
} finally {
40-
if (!process.env.GATSBY_TEST_SKIP_CLEANUP) {
41-
console.log(`Deleting project with deploy_id ${deployInfo.deploy_id}`)
42-
43-
const deleteResponse = await execa("ntl", [
44-
"api",
45-
"deleteDeploy",
46-
"--data",
47-
`{ "deploy_id": "${deployInfo.deploy_id}" }`,
48-
])
49-
50-
if (deleteResponse.exitCode !== 0) {
51-
throw new Error(
52-
`Failed to delete project ${deleteResponse.stdout} ${deleteResponse.stderr} (${deleteResponse.exitCode})`
53-
)
54-
}
55-
56-
console.log(
57-
`Successfully deleted project with deploy_id ${deployInfo.deploy_id}`
58-
)
59-
}
40+
// if (!process.env.GATSBY_TEST_SKIP_CLEANUP) {
41+
// console.log(`Deleting project with deploy_id ${deployInfo.deploy_id}`)
42+
// const deleteResponse = await execa("ntl", [
43+
// "api",
44+
// "deleteDeploy",
45+
// "--data",
46+
// `{ "deploy_id": "${deployInfo.deploy_id}" }`,
47+
// ])
48+
// if (deleteResponse.exitCode !== 0) {
49+
// throw new Error(
50+
// `Failed to delete project ${deleteResponse.stdout} ${deleteResponse.stderr} (${deleteResponse.exitCode})`
51+
// )
52+
// }
53+
// console.log(
54+
// `Successfully deleted project with deploy_id ${deployInfo.deploy_id}`
55+
// )
56+
// }
6057
}

e2e-tests/adapters/src/pages/routes/ssr/static.jsx

+12-6
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ const SSR = ({ serverData }) => {
77
<h1>SSR</h1>
88
<div>
99
<code>
10-
<pre>
11-
{JSON.stringify({ serverData }, null, 2)}
12-
</pre>
10+
<pre>{JSON.stringify({ serverData }, null, 2)}</pre>
1311
</code>
1412
</div>
1513
<div>
1614
<code>
17-
<pre data-testid="query">{JSON.stringify(serverData?.arg?.query)}</pre>
18-
<pre data-testid="params">{JSON.stringify(serverData?.arg?.params)}</pre>
15+
<pre data-testid="query">
16+
{JSON.stringify(serverData?.arg?.query)}
17+
</pre>
18+
<pre data-testid="params">
19+
{JSON.stringify(serverData?.arg?.params)}
20+
</pre>
1921
</code>
2022
</div>
2123
</Layout>
@@ -32,5 +34,9 @@ export function getServerData(arg) {
3234
ssr: true,
3335
arg,
3436
},
37+
headers: {
38+
"x-ssr-header-getserverdata": "my custom header value from getServerData",
39+
"x-ssr-header-overwrite": "getServerData wins",
40+
},
3541
}
36-
}
42+
}

packages/gatsby-adapter-netlify/src/index.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,14 @@ const createNetlifyAdapter: AdapterInit<INetlifyAdapterOptions> = options => {
6262
}
6363
},
6464
},
65-
async adapt({ routesManifest, functionsManifest }): Promise<void> {
65+
async adapt({
66+
routesManifest,
67+
functionsManifest,
68+
headerRoutes,
69+
}): Promise<void> {
6670
const { lambdasThatUseCaching } = await handleRoutesManifest(
67-
routesManifest
71+
routesManifest,
72+
headerRoutes
6873
)
6974

7075
// functions handling

0 commit comments

Comments
 (0)