Skip to content

Commit 2396192

Browse files
authored
feat: add support for custom error pages (#244)
1 parent 25146ae commit 2396192

File tree

7 files changed

+133
-37
lines changed

7 files changed

+133
-37
lines changed

demo/src/pages/500.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react'
2+
3+
// styles
4+
const pageStyles = {
5+
color: '#232129',
6+
padding: '96px',
7+
fontFamily: '-apple-system, Roboto, sans-serif, serif',
8+
}
9+
const headingStyles = {
10+
marginTop: 0,
11+
marginBottom: 64,
12+
maxWidth: 320,
13+
}
14+
15+
const paragraphStyles = {
16+
marginBottom: 48,
17+
}
18+
19+
const ErrorPage = () => {
20+
return (
21+
<main style={pageStyles}>
22+
<title>Internal Server Error</title>
23+
<h1 style={headingStyles}>Internal Server Error</h1>
24+
<p style={paragraphStyles}>
25+
Sorry{' '}
26+
<span role="img" aria-label="Pensive emoji">
27+
😔
28+
</span>{' '}
29+
there was an error.
30+
</p>
31+
</main>
32+
)
33+
}
34+
35+
export default ErrorPage

demo/src/pages/bad-dog.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from 'react'
2+
import { Layout } from '../layout/default'
3+
4+
const BadPage = ({ serverData }) => (
5+
<Layout>
6+
<h1>SSR Page with a random dog</h1>
7+
<img alt="Happy dog" src={serverData.message} />
8+
</Layout>
9+
)
10+
11+
export default BadPage
12+
13+
export async function getServerData() {
14+
throw new Error('Bad dog')
15+
}

demo/src/pages/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ const links = [
3838
{ text: 'Blog', url: 'blog', description: 'DSG demo' },
3939
{ text: 'Dog of the day', url: 'dog', description: 'SSR demo' },
4040
{ text: 'Hello World', url: 'api/hello-world', description: '' },
41+
{
42+
text: 'Error Page',
43+
url: 'bad-dog',
44+
description: 'SSR page that throws an error',
45+
},
46+
4147
{
4248
text: 'I Am Capitalized',
4349
url: 'api/I-Am-Capitalized',

plugin/src/helpers/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export function mutateConfig({
107107
netlifyConfig.functions.__dsg = {
108108
included_files: [
109109
'public/404.html',
110+
'public/500.html',
110111
path.posix.join(CACHE_DIR, 'data', '**'),
111112
path.posix.join(CACHE_DIR, 'query-engine', '**'),
112113
path.posix.join(CACHE_DIR, 'page-ssr', '**'),

plugin/src/templates/handlers.ts

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type { IGatsbyPage } from 'gatsby/cache-dir/query-engine'
1010

1111
// These are "require()"d rather than imported so the symbol names are not munged,
1212
// as we need them to match the hard-coded values
13-
const { readFileSync } = require('fs')
1413
const { join } = require('path')
1514

1615
const etag = require('etag')
@@ -19,6 +18,7 @@ const {
1918
getPagePathFromPageDataPath,
2019
getGraphQLEngine,
2120
prepareFilesystem,
21+
getErrorResponse,
2222
} = require('./utils')
2323

2424
type SSRReq = Pick<GatsbyFunctionRequest, 'query' | 'method' | 'url'> & {
@@ -39,6 +39,8 @@ export type RenderMode = 'SSR' | 'DSG'
3939
* the actual handler code, with the correct paths and render mode injected.
4040
*/
4141
const getHandler = (renderMode: RenderMode, appDir: string): Handler => {
42+
process.chdir(appDir)
43+
4244
const DATA_SUFFIX = '/page-data.json'
4345
const DATA_PREFIX = '/page-data/'
4446
const cacheDir = join(appDir, '.cache')
@@ -68,17 +70,7 @@ const getHandler = (renderMode: RenderMode, appDir: string): Handler => {
6870
graphqlEngine.findPageByPath(pathName)
6971

7072
if (page?.mode !== renderMode) {
71-
const body = readFileSync(join(appDir, 'public', '404.html'), 'utf8')
72-
73-
return {
74-
statusCode: 404,
75-
body,
76-
headers: {
77-
Tag: etag(body),
78-
'Content-Type': 'text/html; charset=utf-8',
79-
'X-Mode': renderMode,
80-
},
81-
}
73+
return getErrorResponse({ statusCode: 404, renderMode })
8274
}
8375
const req: SSRReq =
8476
renderMode === 'SSR'
@@ -95,48 +87,52 @@ const getHandler = (renderMode: RenderMode, appDir: string): Handler => {
9587
headers: {},
9688
}
9789

98-
const data = await getData({
99-
pathName,
100-
graphqlEngine,
101-
req,
102-
})
90+
console.log(`[${req.method}] ${event.path} (${renderMode})`)
91+
92+
try {
93+
const data = await getData({
94+
pathName,
95+
graphqlEngine,
96+
req,
97+
})
98+
if (isPageData) {
99+
const body = JSON.stringify(await renderPageData({ data }))
100+
return {
101+
statusCode: 200,
102+
body,
103+
headers: {
104+
ETag: etag(body),
105+
'Content-Type': 'application/json',
106+
'X-Render-Mode': renderMode,
107+
...data.serverDataHeaders,
108+
},
109+
}
110+
}
111+
112+
const body = await renderHTML({ data })
103113

104-
if (isPageData) {
105-
const body = JSON.stringify(await renderPageData({ data }))
106114
return {
107115
statusCode: 200,
108116
body,
109117
headers: {
110118
ETag: etag(body),
111-
'Content-Type': 'application/json',
112-
'X-Mode': renderMode,
119+
'Content-Type': 'text/html; charset=utf-8',
120+
'X-Render-Mode': renderMode,
113121
...data.serverDataHeaders,
114122
},
115123
}
116-
}
117-
118-
const body = await renderHTML({ data })
119-
120-
return {
121-
statusCode: 200,
122-
body,
123-
headers: {
124-
ETag: etag(body),
125-
'Content-Type': 'text/html; charset=utf-8',
126-
'X-Mode': renderMode,
127-
...data.serverDataHeaders,
128-
},
124+
} catch (error) {
125+
return getErrorResponse({ error, renderMode })
129126
}
130127
}
131128
}
132129

133130
export const makeHandler = (appDir: string, renderMode: RenderMode): string =>
134131
// This is a string, but if you have the right editor plugin it should format as js
135132
javascript`
136-
// @ts-check
137133
const { readFileSync } = require('fs');
138134
const { builder } = require('@netlify/functions');
139-
const { getPagePathFromPageDataPath, getGraphQLEngine, prepareFilesystem } = require('./utils')
135+
const { getPagePathFromPageDataPath, getGraphQLEngine, prepareFilesystem, getErrorResponse } = require('./utils')
140136
const { join, resolve } = require("path");
141137
const etag = require('etag');
142138
const pageRoot = resolve(join(__dirname, "${appDir}"));

plugin/src/templates/utils.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import os from 'os'
33
import { join } from 'path'
44
import process from 'process'
55

6-
import { existsSync, copySync } from 'fs-extra'
6+
import { HandlerResponse } from '@netlify/functions'
7+
import etag from 'etag'
8+
import { existsSync, copySync, readFileSync } from 'fs-extra'
79
import type { GraphQLEngine } from 'gatsby/cache-dir/query-engine'
810
import { link } from 'linkfs'
911

@@ -23,6 +25,7 @@ declare global {
2325
* Hacks to deal with the fact that functions execute on a readonly filesystem
2426
*/
2527
export function prepareFilesystem(cacheDir: string): void {
28+
console.log('Preparing Gatsby filesystem')
2629
const rewrites = [
2730
[join(cacheDir, 'caches'), join(TEMP_CACHE_DIR, 'caches')],
2831
[join(cacheDir, 'caches-lmdb'), join(TEMP_CACHE_DIR, 'caches-lmdb')],
@@ -86,3 +89,41 @@ export function getGraphQLEngine(cacheDir: string): GraphQLEngine {
8689
dbPath,
8790
})
8891
}
92+
93+
/**
94+
* Gets an error page to return from a function
95+
*/
96+
97+
export function getErrorResponse({
98+
statusCode = 500,
99+
error,
100+
renderMode,
101+
}: {
102+
statusCode?: number
103+
error?: Error
104+
renderMode: 'DSG' | 'SSR'
105+
}): HandlerResponse {
106+
let body = `<html><body><h1>${statusCode}</h1><p>${
107+
statusCode === 404 ? 'Not found' : 'Internal Server Error'
108+
}</p></body></html>`
109+
110+
if (error) {
111+
console.error(error)
112+
}
113+
114+
if (statusCode === 500 || statusCode === 404) {
115+
const filename = join(process.cwd(), 'public', `${statusCode}.html`)
116+
if (existsSync(filename)) {
117+
body = readFileSync(filename, 'utf8')
118+
}
119+
}
120+
return {
121+
statusCode,
122+
body,
123+
headers: {
124+
Tag: etag(body),
125+
'Content-Type': 'text/html; charset=utf-8',
126+
'X-Render-Mode': renderMode,
127+
},
128+
}
129+
}

plugin/test/fixtures/functions-without-gatsby-plugin/e2e-tests/test-helpers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const FormData = require('form-data')
88
// Source: https://github.com/gatsbyjs/gatsby/blob/master/integration-tests/functions/test-helpers.js
99

1010
exports.runTests = function runTests(env, host) {
11+
jest.setTimeout(10_000)
12+
1113
async function fetchTwice(url, options) {
1214
const result = await fetch(url, options)
1315
if (!result.headers.has('x-forwarded-host')) {

0 commit comments

Comments
 (0)