Skip to content

Commit 451bf78

Browse files
authored
feat(gatsby): add support for 500.html (#33488)
* feat(gatsby): add support for 500.html * add e2e and some fixes to runtime * fix tests (?) * paths better start with / * move warn-once to separate module and use it for 500/404 status pages non-SSG warnings * rename internal plugin to mention 500, update some comments
1 parent 4e78c61 commit 451bf78

File tree

18 files changed

+203
-77
lines changed

18 files changed

+203
-77
lines changed

e2e-tests/production-runtime/cypress/integration/ssr.js

+26
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,29 @@ describe(`Wildcard path ('${wildcardPath}*')`, () => {
106106
cy.getTestElement(`params`).contains(`{"wildcard":"baz"}`)
107107
})
108108
})
109+
110+
describe(`500 status`, () => {
111+
const errorPath = `/ssr/error-path/`
112+
113+
it(`Display 500 page on direct navigation`, () => {
114+
cy.visit(errorPath, { failOnStatusCode: false }).waitForRouteChange()
115+
116+
cy.location(`pathname`)
117+
.should(`equal`, errorPath)
118+
.getTestElement(`500`)
119+
.should(`exist`)
120+
})
121+
122+
it(`Display 500 page on client navigation`, () => {
123+
cy.visit(`/`).waitForRouteChange()
124+
125+
cy.window()
126+
.then(win => win.___navigate(errorPath))
127+
.waitForRouteChange()
128+
129+
cy.location(`pathname`)
130+
.should(`equal`, errorPath)
131+
.getTestElement(`500`)
132+
.should(`exist`)
133+
})
134+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as React from "react"
2+
import { Link } from "gatsby"
3+
4+
import Layout from "../components/layout"
5+
6+
const InternalServerErrorPage = () => (
7+
<Layout>
8+
<h1 data-testid="500">INTERNAL SERVER ERROR</h1>
9+
<p>Page could not be displayed</p>
10+
<pre data-testid="dom-marker">500</pre>
11+
<Link to="/" data-testid="index">
12+
Go to Index
13+
</Link>
14+
</Layout>
15+
)
16+
17+
export default InternalServerErrorPage
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react"
2+
3+
export default function ErrorPath({ serverData }) {
4+
return (
5+
<div>
6+
<div>This will never render</div>
7+
</div>
8+
)
9+
}
10+
11+
export function getServerData() {
12+
throw new Error(`Some runtime error`)
13+
}

packages/gatsby/cache-dir/__tests__/dev-loader.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,16 @@ describe(`Dev loader`, () => {
205205

206206
const expectation = {
207207
status: `error`,
208-
pagePath: `/error-page`,
208+
pagePath: `/500.html`,
209+
internalServerError: true,
210+
retries: 3,
209211
}
212+
210213
expect(await devLoader.loadPageDataJson(`/error-page/`)).toEqual({
211214
status: `error`,
212215
pagePath: `/dev-404-page`,
213216
retries: 3,
217+
internalServerError: true,
214218
})
215219
expect(devLoader.pageDataDb.get(`/error-page`)).toEqual(expectation)
216220
expect(xhrCount).toBe(1)

packages/gatsby/cache-dir/__tests__/loader.js

+32-2
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,16 @@ describe(`Production loader`, () => {
187187
expect(xhrCount).toBe(2)
188188
})
189189

190-
it(`should return an error when status is 500`, async () => {
190+
it(`should return an error when status is 500 and 500.html is not available`, async () => {
191191
const prodLoader = new ProdLoader(null, [])
192192

193193
mockPageData(`/error-page`, 500)
194194

195195
const expectation = {
196196
status: `error`,
197-
pagePath: `/error-page`,
197+
pagePath: `/500.html`,
198+
internalServerError: true,
199+
retries: 3,
198200
}
199201
expect(await prodLoader.loadPageDataJson(`/error-page/`)).toEqual(
200202
expectation
@@ -203,6 +205,34 @@ describe(`Production loader`, () => {
203205
expect(xhrCount).toBe(1)
204206
})
205207

208+
it(`should return an error when status is 500 and 500.html is available`, async () => {
209+
const prodLoader = new ProdLoader(null, [])
210+
211+
mockPageData(`/error-page`, 500)
212+
mockPageData(
213+
`/500.html`,
214+
200,
215+
{
216+
path: `/500.html`,
217+
},
218+
true
219+
)
220+
221+
const expectation = {
222+
status: `success`,
223+
pagePath: `/500.html`,
224+
internalServerError: true,
225+
payload: {
226+
path: `/500.html`,
227+
},
228+
}
229+
expect(await prodLoader.loadPageDataJson(`/error-page/`)).toEqual(
230+
expectation
231+
)
232+
expect(prodLoader.pageDataDb.get(`/error-page`)).toEqual(expectation)
233+
expect(xhrCount).toBe(2)
234+
})
235+
206236
it(`should retry 3 times before returning an error`, async () => {
207237
const prodLoader = new ProdLoader(null, [])
208238

packages/gatsby/cache-dir/loader.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ export class BaseLoader {
161161

162162
// Handle 404
163163
if (status === 404 || status === 200) {
164-
// If the request was for a 404 page and it doesn't exist, we're done
165-
if (pagePath === `/404.html`) {
164+
// If the request was for a 404/500 page and it doesn't exist, we're done
165+
if (pagePath === `/404.html` || pagePath === `/500.html`) {
166166
return Object.assign(loadObj, {
167167
status: PageResourceStatus.Error,
168168
})
@@ -177,9 +177,12 @@ export class BaseLoader {
177177

178178
// handle 500 response (Unrecoverable)
179179
if (status === 500) {
180-
return Object.assign(loadObj, {
181-
status: PageResourceStatus.Error,
182-
})
180+
return this.fetchPageDataJson(
181+
Object.assign(loadObj, {
182+
pagePath: `/500.html`,
183+
internalServerError: true,
184+
})
185+
)
183186
}
184187

185188
// Handle everything else, including status === 0, and 503s. Should retry

packages/gatsby/cache-dir/production-app.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ apiRunnerAsync(`onClientEntry`).then(() => {
104104
>
105105
<RouteHandler
106106
path={
107-
pageResources.page.path === `/404.html`
107+
pageResources.page.path === `/404.html` ||
108+
pageResources.page.path === `/500.html`
108109
? stripPrefix(location.pathname, __BASE_PATH__)
109110
: encodeURI(
110111
(
@@ -144,8 +145,7 @@ apiRunnerAsync(`onClientEntry`).then(() => {
144145
browserLoc.pathname + (pagePath.includes(`?`) ? browserLoc.search : ``) &&
145146
!(
146147
loader.findMatchPath(stripPrefix(browserLoc.pathname, __BASE_PATH__)) ||
147-
pagePath === `/404.html` ||
148-
pagePath.match(/^\/404\/?$/) ||
148+
pagePath.match(/^\/(404|500)(\/?|.html)$/) ||
149149
pagePath.match(/^\/offline-plugin-app-shell-fallback\/?$/)
150150
)
151151
) {

packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.ts.snap

+6-6
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Array [
4848
Object {
4949
"browserAPIs": Array [],
5050
"id": "",
51-
"name": "prod-404",
51+
"name": "prod-404-500",
5252
"nodeAPIs": Array [],
5353
"pluginOptions": Object {
5454
"plugins": Array [],
@@ -176,7 +176,7 @@ Array [
176176
"onPluginInit",
177177
],
178178
"pluginOptions": Object {
179-
"path": "<PROJECT_ROOT>/packages/gatsby/src/internal-plugins/prod-404/src/pages",
179+
"path": "<PROJECT_ROOT>/packages/gatsby/src/internal-plugins/prod-404-500/src/pages",
180180
"pathCheck": false,
181181
"plugins": Array [],
182182
},
@@ -355,7 +355,7 @@ Array [
355355
Object {
356356
"browserAPIs": Array [],
357357
"id": "",
358-
"name": "prod-404",
358+
"name": "prod-404-500",
359359
"nodeAPIs": Array [],
360360
"pluginOptions": Object {
361361
"plugins": Array [],
@@ -495,7 +495,7 @@ Array [
495495
"onPluginInit",
496496
],
497497
"pluginOptions": Object {
498-
"path": "<PROJECT_ROOT>/packages/gatsby/src/internal-plugins/prod-404/src/pages",
498+
"path": "<PROJECT_ROOT>/packages/gatsby/src/internal-plugins/prod-404-500/src/pages",
499499
"pathCheck": false,
500500
"plugins": Array [],
501501
},
@@ -695,7 +695,7 @@ Array [
695695
Object {
696696
"browserAPIs": Array [],
697697
"id": "",
698-
"name": "prod-404",
698+
"name": "prod-404-500",
699699
"nodeAPIs": Array [],
700700
"pluginOptions": Object {
701701
"plugins": Array [],
@@ -846,7 +846,7 @@ Array [
846846
"onPluginInit",
847847
],
848848
"pluginOptions": Object {
849-
"path": "<PROJECT_ROOT>/packages/gatsby/src/internal-plugins/prod-404/src/pages",
849+
"path": "<PROJECT_ROOT>/packages/gatsby/src/internal-plugins/prod-404-500/src/pages",
850850
"pathCheck": false,
851851
"plugins": Array [],
852852
},

packages/gatsby/src/bootstrap/load-plugins/load.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ export function loadPlugins(
268268
`../../internal-plugins/dev-404-page`,
269269
`../../internal-plugins/load-babel-config`,
270270
`../../internal-plugins/internal-data-bridge`,
271-
`../../internal-plugins/prod-404`,
271+
`../../internal-plugins/prod-404-500`,
272272
`../../internal-plugins/webpack-theme-component-shadowing`,
273273
`../../internal-plugins/bundle-optimisations`,
274274
`../../internal-plugins/functions`,

packages/gatsby/src/commands/serve.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,11 @@ module.exports = async (program: IServeProgram): Promise<void> => {
298298
`Rendering html for "${potentialPagePath}" failed.`,
299299
e
300300
)
301-
return res
302-
.status(500)
303-
.contentType(`text/plain`)
304-
.send(`Internal server error.`)
301+
return res.status(500).sendFile(`500.html`, { root }, err => {
302+
if (err) {
303+
res.contentType(`text/plain`).send(`Internal server error.`)
304+
}
305+
})
305306
}
306307
}
307308
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const { emitter, store } = require(`../../redux`)
2+
const { actions } = require(`../../redux/actions`)
3+
4+
const originalStatusPageByStatus = {}
5+
const originalStatusPageByPath = {}
6+
7+
emitter.on(`CREATE_PAGE`, action => {
8+
// Copy /404/ to /404.html and /500/ to /500.html. Many static site hosts expect
9+
// site 404 pages to be named this. In addition, with Rendering Engines there might
10+
// be runtime errors which would fallback to "/500.html" page.
11+
// https://www.gatsbyjs.org/docs/how-to/adding-common-features/add-404-page/
12+
const result = /^\/?(404|500)\/?$/.exec(action.payload.path)
13+
if (result && result.length > 1) {
14+
const status = result[1]
15+
16+
const originalPage = originalStatusPageByStatus[status]
17+
18+
if (!originalPage) {
19+
const storedPage = {
20+
path: action.payload.path,
21+
component: action.payload.component,
22+
context: action.payload.context,
23+
status,
24+
}
25+
26+
originalStatusPageByStatus[status] = storedPage
27+
originalStatusPageByPath[action.payload.path] = storedPage
28+
29+
store.dispatch(
30+
actions.createPage(
31+
{
32+
...storedPage,
33+
path: `/${status}.html`,
34+
},
35+
action.plugin
36+
)
37+
)
38+
}
39+
}
40+
})
41+
42+
emitter.on(`DELETE_PAGE`, action => {
43+
const storedPage = originalStatusPageByPath[action.payload.path]
44+
if (storedPage) {
45+
store.dispatch(
46+
actions.deletePage({
47+
...storedPage,
48+
path: `/${storedPage.status}.html`,
49+
})
50+
)
51+
originalStatusPageByPath[action.payload.path] = null
52+
originalStatusPageByStatus[storedPage.status] = null
53+
}
54+
})

packages/gatsby/src/internal-plugins/prod-404/package.json renamed to packages/gatsby/src/internal-plugins/prod-404-500/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "prod-404",
2+
"name": "prod-404-500",
33
"version": "1.0.0",
44
"description": "Internal plugin to detect various flavors of 404 pages and ensure there's a 404.html path created as well to ensure compatibility with static hosts",
55
"main": "index.js",

packages/gatsby/src/internal-plugins/prod-404/gatsby-node.js

-39
This file was deleted.

packages/gatsby/src/redux/actions/public.js

+1-9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const { getPageMode } = require(`../../utils/page-mode`)
2727
const normalizePath = require(`../../utils/normalize-path`).default
2828
import { createJobV2FromInternalJob } from "./internal"
2929
import { maybeSendJobToMainProcess } from "../../utils/jobs/worker-messaging"
30+
import { warnOnce } from "../../utils/warn-once"
3031
import fs from "fs-extra"
3132

3233
const isNotTestEnv = process.env.NODE_ENV !== `test`
@@ -73,15 +74,6 @@ const findChildren = initialChildren => {
7374
return children
7475
}
7576

76-
const displayedWarnings = new Set()
77-
const warnOnce = (message, key) => {
78-
const messageId = key ?? message
79-
if (!displayedWarnings.has(messageId)) {
80-
displayedWarnings.add(messageId)
81-
report.warn(message)
82-
}
83-
}
84-
8577
import type { Plugin } from "./types"
8678

8779
type Job = {

0 commit comments

Comments
 (0)