Skip to content

Commit

Permalink
fix(gatsby-adapter-netlify): adapter use headerRoutes (#38652) (#38674)
Browse files Browse the repository at this point in the history
* 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 <misiek.piechowiak@gmail.com>
(cherry picked from commit 22c2412)

Co-authored-by: Katherine Beck <49894658+kathmbeck@users.noreply.github.com>
  • Loading branch information
gatsbybot and kathmbeck committed Oct 31, 2023
1 parent 8ae8281 commit 8ae5702
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 64 deletions.
14 changes: 4 additions & 10 deletions e2e-tests/adapters/cypress/e2e/basics.cy.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { title } from "../../constants"
import { WorkaroundCachedResponse } from "../utils/dont-cache-responses-in-browser"

describe("Basics", () => {
beforeEach(() => {
cy.intercept("/gatsby-icon.png").as("static-folder-image")
cy.intercept("/static/astro-**.png", req => {
req.on("before:response", res => {
// this generally should be permamently cached, but that cause problems with intercepting
// see https://docs.cypress.io/api/commands/intercept#cyintercept-and-request-caching
// so we disable caching for this response
// tests for cache-control headers should be done elsewhere

res.headers["cache-control"] = "no-store"
})
}).as("img-import")
cy.intercept("/static/astro-**.png", WorkaroundCachedResponse).as(
"img-import"
)

cy.visit("/").waitForRouteChange()
})
Expand Down
147 changes: 147 additions & 0 deletions e2e-tests/adapters/cypress/e2e/headers.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { WorkaroundCachedResponse } from "../utils/dont-cache-responses-in-browser"

describe("Headers", () => {
const defaultHeaders = {
"x-xss-protection": "1; mode=block",
"x-content-type-options": "nosniff",
"referrer-policy": "same-origin",
"x-frame-options": "DENY",
}

// DRY for repeated assertions in multple tests
const expectedHeadersByRouteAlias = {
"@app-data": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@page-data": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@slice-data": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@static-query-result": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@img-webpack-import": {
...defaultHeaders,
"cache-control": "public,max-age=31536000,immutable",
},
"@js": {
...defaultHeaders,
"cache-control": "public,max-age=31536000,immutable",
},
}

// `ntl serve` and actual deploy seem to have possible slight differences around header value formatting
// so this just remove spaces around commas to make it easier to compare
function normalizeHeaderValue(value: string | undefined): string | undefined {
if (typeof value === "undefined") {
return value
}
// Remove spaces around commas
return value.replace(/\s*,\s*/gm, `,`)
}
function checkHeaders(
routeAlias: string,
expectedHeaders?: Record<string, string>
) {
if (!expectedHeaders) {
expectedHeaders = expectedHeadersByRouteAlias[routeAlias]
}

if (!expectedHeaders) {
throw new Error(`No expected headers provided for "${routeAlias}`)
}

cy.wait(routeAlias).then(interception => {
Object.keys(expectedHeaders).forEach(headerKey => {
const headers = interception.response.headers[headerKey]

const firstHeader: string = Array.isArray(headers)
? headers[0]
: headers

expect(normalizeHeaderValue(firstHeader)).to.eq(
normalizeHeaderValue(expectedHeaders[headerKey])
)
})
})
}

beforeEach(() => {
cy.intercept("/", WorkaroundCachedResponse).as("index")
cy.intercept("routes/ssr/static", WorkaroundCachedResponse).as("ssr")
cy.intercept("routes/dsg/static", WorkaroundCachedResponse).as("dsg")

cy.intercept("**/page-data.json", WorkaroundCachedResponse).as("page-data")
cy.intercept("**/app-data.json", WorkaroundCachedResponse).as("app-data")
cy.intercept("**/slice-data/*.json", WorkaroundCachedResponse).as(
"slice-data"
)
cy.intercept("**/page-data/sq/d/*.json", WorkaroundCachedResponse).as(
"static-query-result"
)

cy.intercept("/static/astro-**.png", WorkaroundCachedResponse).as(
"img-webpack-import"
)
cy.intercept("*.js", WorkaroundCachedResponse).as("js")
})

it("should contain correct headers for index page", () => {
cy.visit("/").waitForRouteChange()

checkHeaders("@index", {
...defaultHeaders,
"x-custom-header": "my custom header value",
"cache-control": "public,max-age=0,must-revalidate",
})

checkHeaders("@app-data")
checkHeaders("@page-data")
checkHeaders("@slice-data")
checkHeaders("@static-query-result")

// index page is only one showing webpack imported image
checkHeaders("@img-webpack-import")
checkHeaders("@js")
})

it("should contain correct headers for ssr page", () => {
cy.visit("routes/ssr/static").waitForRouteChange()

checkHeaders("@ssr", {
...defaultHeaders,
"x-custom-header": "my custom header value",
"x-ssr-header": "my custom header value from config",
"x-ssr-header-getserverdata": "my custom header value from getServerData",
"x-ssr-header-overwrite": "getServerData wins",
})

checkHeaders("@app-data")
// page-data is baked into SSR page so it's not fetched and we don't assert it
checkHeaders("@slice-data")
checkHeaders("@static-query-result")
checkHeaders("@js")
})

it("should contain correct headers for dsg page", () => {
cy.visit("routes/dsg/static").waitForRouteChange()

checkHeaders("@dsg", {
...defaultHeaders,
"x-custom-header": "my custom header value",
"x-dsg-header": "my custom header value",
})

checkHeaders("@app-data")
checkHeaders("@page-data")
checkHeaders("@slice-data")
checkHeaders("@static-query-result")
checkHeaders("@js")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CyHttpMessages } from "cypress/types/net-stubbing"

/**
* https://docs.cypress.io/api/commands/intercept#cyintercept-and-request-caching
*
* For responses that are to be cached we need to use a trick so browser doesn't cache them
* So this enforces `no-store` cache-control header before response hits the browser
* and then restore original cache-control value for assertions.
*/
export const WorkaroundCachedResponse = (
req: CyHttpMessages.IncomingHttpRequest
): void | Promise<void> => {
req.on("before:response", res => {
res.headers["x-original-cache-control"] = res.headers["cache-control"]
res.headers["cache-control"] = "no-store"
})
req.on("after:response", res => {
res.headers["cache-control"] = res.headers["x-original-cache-control"]
delete res.headers["x-original-cache-control"]
})
}
34 changes: 21 additions & 13 deletions e2e-tests/adapters/debug-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { inspect } from "util"
import type { AdapterInit } from "gatsby"

const createTestingAdapter: AdapterInit = (adapterOptions) => {
const createTestingAdapter: AdapterInit = adapterOptions => {
return {
name: `gatsby-adapter-debug`,
cache: {
Expand All @@ -10,28 +10,36 @@ const createTestingAdapter: AdapterInit = (adapterOptions) => {
},
store({ directories, reporter }) {
reporter.info(`[gatsby-adapter-debug] cache.store() ${directories}`)
}
},
},
adapt({
routesManifest,
headerRoutes,
functionsManifest,
pathPrefix,
trailingSlash,
reporter,
}) {
reporter.info(`[gatsby-adapter-debug] adapt()`)

console.log(`[gatsby-adapter-debug] adapt()`, inspect({
routesManifest,
functionsManifest,
pathPrefix,
trailingSlash,
}, {
depth: Infinity,
colors: true
}))
}
console.log(
`[gatsby-adapter-debug] adapt()`,
inspect(
{
routesManifest,
headerRoutes,
functionsManifest,
pathPrefix,
trailingSlash,
},
{
depth: Infinity,
colors: true,
}
)
)
},
}
}

export default createTestingAdapter
export default createTestingAdapter
36 changes: 35 additions & 1 deletion e2e-tests/adapters/gatsby-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import debugAdapter from "./debug-adapter"
import { siteDescription, title } from "./constants"

const shouldUseDebugAdapter = process.env.USE_DEBUG_ADAPTER ?? false
const trailingSlash = (process.env.TRAILING_SLASH || `never`) as GatsbyConfig["trailingSlash"]
const trailingSlash = (process.env.TRAILING_SLASH ||
`never`) as GatsbyConfig["trailingSlash"]

let configOverrides: GatsbyConfig = {}

Expand All @@ -21,6 +22,39 @@ const config: GatsbyConfig = {
},
trailingSlash,
plugins: [],
headers: [
{
source: `/*`,
headers: [
{
key: "x-custom-header",
value: "my custom header value",
},
],
},
{
source: `routes/ssr/*`,
headers: [
{
key: "x-ssr-header",
value: "my custom header value from config",
},
{
key: "x-ssr-header-overwrite",
value: "config wins",
},
],
},
{
source: `routes/dsg/*`,
headers: [
{
key: "x-dsg-header",
value: "my custom header value",
},
],
},
],
...configOverrides,
}

Expand Down
37 changes: 17 additions & 20 deletions e2e-tests/adapters/scripts/deploy-and-run/netlify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,21 @@ console.log(`Deployed to ${deployInfo.deploy_url}`)
try {
await execa(`npm`, [`run`, npmScriptToRun], { stdio: `inherit` })
} finally {
if (!process.env.GATSBY_TEST_SKIP_CLEANUP) {
console.log(`Deleting project with deploy_id ${deployInfo.deploy_id}`)

const deleteResponse = await execa("ntl", [
"api",
"deleteDeploy",
"--data",
`{ "deploy_id": "${deployInfo.deploy_id}" }`,
])

if (deleteResponse.exitCode !== 0) {
throw new Error(
`Failed to delete project ${deleteResponse.stdout} ${deleteResponse.stderr} (${deleteResponse.exitCode})`
)
}

console.log(
`Successfully deleted project with deploy_id ${deployInfo.deploy_id}`
)
}
// if (!process.env.GATSBY_TEST_SKIP_CLEANUP) {
// console.log(`Deleting project with deploy_id ${deployInfo.deploy_id}`)
// const deleteResponse = await execa("ntl", [
// "api",
// "deleteDeploy",
// "--data",
// `{ "deploy_id": "${deployInfo.deploy_id}" }`,
// ])
// if (deleteResponse.exitCode !== 0) {
// throw new Error(
// `Failed to delete project ${deleteResponse.stdout} ${deleteResponse.stderr} (${deleteResponse.exitCode})`
// )
// }
// console.log(
// `Successfully deleted project with deploy_id ${deployInfo.deploy_id}`
// )
// }
}
18 changes: 12 additions & 6 deletions e2e-tests/adapters/src/pages/routes/ssr/static.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ const SSR = ({ serverData }) => {
<h1>SSR</h1>
<div>
<code>
<pre>
{JSON.stringify({ serverData }, null, 2)}
</pre>
<pre>{JSON.stringify({ serverData }, null, 2)}</pre>
</code>
</div>
<div>
<code>
<pre data-testid="query">{JSON.stringify(serverData?.arg?.query)}</pre>
<pre data-testid="params">{JSON.stringify(serverData?.arg?.params)}</pre>
<pre data-testid="query">
{JSON.stringify(serverData?.arg?.query)}
</pre>
<pre data-testid="params">
{JSON.stringify(serverData?.arg?.params)}
</pre>
</code>
</div>
</Layout>
Expand All @@ -32,5 +34,9 @@ export function getServerData(arg) {
ssr: true,
arg,
},
headers: {
"x-ssr-header-getserverdata": "my custom header value from getServerData",
"x-ssr-header-overwrite": "getServerData wins",
},
}
}
}
9 changes: 7 additions & 2 deletions packages/gatsby-adapter-netlify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,14 @@ const createNetlifyAdapter: AdapterInit<INetlifyAdapterOptions> = options => {
}
},
},
async adapt({ routesManifest, functionsManifest }): Promise<void> {
async adapt({
routesManifest,
functionsManifest,
headerRoutes,
}): Promise<void> {
const { lambdasThatUseCaching } = await handleRoutesManifest(
routesManifest
routesManifest,
headerRoutes
)

// functions handling
Expand Down

0 comments on commit 8ae5702

Please sign in to comment.