Skip to content

Commit 0cc6ede

Browse files
okikiomatthewp
andauthored
SSR 404 and 500 routes in adapters (#4018)
* fix(WIP): SSR 404 and 500 routes * Implement the feature Co-authored-by: Matthew Phillips <matthew@skypack.dev>
1 parent 4392083 commit 0cc6ede

14 files changed

Lines changed: 124 additions & 21 deletions

File tree

.changeset/happy-parrots-stare.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'astro': minor
3+
'@astrojs/cloudflare': minor
4+
'@astrojs/netlify': minor
5+
'@astrojs/vercel': minor
6+
---
7+
8+
Support for 404 and 500 pages in SSR

packages/astro/src/core/app/index.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export { deserializeManifest } from './common.js';
2525
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
2626
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
2727

28+
export interface MatchOptions {
29+
matchNotFound?: boolean | undefined;
30+
}
31+
2832
export class App {
2933
#manifest: Manifest;
3034
#manifestData: ManifestData;
@@ -46,17 +50,30 @@ export class App {
4650
this.#routeCache = new RouteCache(this.#logging);
4751
this.#streaming = streaming;
4852
}
49-
match(request: Request): RouteData | undefined {
53+
match(request: Request, { matchNotFound = false }: MatchOptions = {}): RouteData | undefined {
5054
const url = new URL(request.url);
5155
// ignore requests matching public assets
5256
if (this.#manifest.assets.has(url.pathname)) {
5357
return undefined;
5458
}
55-
return matchRoute(url.pathname, this.#manifestData);
59+
let routeData = matchRoute(url.pathname, this.#manifestData);
60+
61+
if(routeData) {
62+
return routeData;
63+
} else if(matchNotFound) {
64+
return matchRoute('/404', this.#manifestData);
65+
} else {
66+
return undefined;
67+
}
5668
}
5769
async render(request: Request, routeData?: RouteData): Promise<Response> {
70+
let defaultStatus = 200;
5871
if (!routeData) {
5972
routeData = this.match(request);
73+
if (!routeData) {
74+
defaultStatus = 404;
75+
routeData = this.match(request, { matchNotFound: true });
76+
}
6077
if (!routeData) {
6178
return new Response(null, {
6279
status: 404,
@@ -65,12 +82,25 @@ export class App {
6582
}
6683
}
6784

68-
const mod = this.#manifest.pageMap.get(routeData.component)!;
85+
let mod = this.#manifest.pageMap.get(routeData.component)!;
6986

7087
if (routeData.type === 'page') {
71-
return this.#renderPage(request, routeData, mod);
88+
let response = await this.#renderPage(request, routeData, mod, defaultStatus);
89+
90+
// If there was a 500 error, try sending the 500 page.
91+
if(response.status === 500) {
92+
const fiveHundredRouteData = matchRoute('/500', this.#manifestData);
93+
if(fiveHundredRouteData) {
94+
mod = this.#manifest.pageMap.get(fiveHundredRouteData.component)!;
95+
try {
96+
let fiveHundredResponse = await this.#renderPage(request, fiveHundredRouteData, mod, 500);
97+
return fiveHundredResponse;
98+
} catch {}
99+
}
100+
}
101+
return response;
72102
} else if (routeData.type === 'endpoint') {
73-
return this.#callEndpoint(request, routeData, mod);
103+
return this.#callEndpoint(request, routeData, mod, defaultStatus);
74104
} else {
75105
throw new Error(`Unsupported route type [${routeData.type}].`);
76106
}
@@ -79,7 +109,8 @@ export class App {
79109
async #renderPage(
80110
request: Request,
81111
routeData: RouteData,
82-
mod: ComponentInstance
112+
mod: ComponentInstance,
113+
status = 200
83114
): Promise<Response> {
84115
const url = new URL(request.url);
85116
const manifest = this.#manifest;
@@ -128,6 +159,7 @@ export class App {
128159
ssr: true,
129160
request,
130161
streaming: this.#streaming,
162+
status
131163
});
132164

133165
return response;
@@ -143,7 +175,8 @@ export class App {
143175
async #callEndpoint(
144176
request: Request,
145177
routeData: RouteData,
146-
mod: ComponentInstance
178+
mod: ComponentInstance,
179+
status = 200
147180
): Promise<Response> {
148181
const url = new URL(request.url);
149182
const handler = mod as unknown as EndpointHandler;
@@ -155,6 +188,7 @@ export class App {
155188
route: routeData,
156189
routeCache: this.#routeCache,
157190
ssr: true,
191+
status
158192
});
159193

160194
if (result.type === 'response') {

packages/astro/src/core/endpoint/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
55

66
export type EndpointOptions = Pick<
77
RenderOptions,
8-
'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr'
8+
'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr' | 'status'
99
>;
1010

1111
type EndpointCallResult =

packages/astro/src/core/render/core.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export interface RenderOptions {
8585
ssr: boolean;
8686
streaming: boolean;
8787
request: Request;
88+
status?: number;
8889
}
8990

9091
export async function render(opts: RenderOptions): Promise<Response> {
@@ -107,6 +108,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
107108
site,
108109
ssr,
109110
streaming,
111+
status = 200
110112
} = opts;
111113

112114
const paramsAndPropsRes = await getParamsAndProps({
@@ -148,6 +150,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
148150
scripts,
149151
ssr,
150152
streaming,
153+
status
151154
});
152155

153156
// Support `export const components` for `MDX` pages

packages/astro/src/core/render/result.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface CreateResultArgs {
4242
scripts?: Set<SSRElement>;
4343
styles?: Set<SSRElement>;
4444
request: Request;
45+
status: number;
4546
}
4647

4748
function getFunctionExpression(slot: any) {
@@ -119,7 +120,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
119120
headers.set('Content-Type', 'text/html');
120121
}
121122
const response: ResponseInit = {
122-
status: 200,
123+
status: args.status,
123124
statusText: 'OK',
124125
headers,
125126
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@test/ssr-api-route-custom-404",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Something went horribly wrong!</h1>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>This is an error page</h1>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
throw new Error(`oops`);
3+
---
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect } from 'chai';
2+
import { loadFixture } from './test-utils.js';
3+
import testAdapter from './test-adapter.js';
4+
import * as cheerio from 'cheerio';
5+
6+
describe('404 and 500 pages', () => {
7+
/** @type {import('./test-utils').Fixture} */
8+
let fixture;
9+
10+
before(async () => {
11+
fixture = await loadFixture({
12+
root: './fixtures/ssr-api-route-custom-404/',
13+
experimental: {
14+
ssr: true,
15+
},
16+
adapter: testAdapter(),
17+
});
18+
await fixture.build({ });
19+
});
20+
21+
it('404 page returned when a route does not match', async () => {
22+
const app = await fixture.loadTestAdapterApp();
23+
const request = new Request('http://example.com/some/fake/route');
24+
const response = await app.render(request);
25+
expect(response.status).to.equal(404);
26+
const html = await response.text();
27+
const $ = cheerio.load(html);
28+
expect($('h1').text()).to.equal('Something went horribly wrong!');
29+
});
30+
31+
it('500 page returned when there is an error', async () => {
32+
const app = await fixture.loadTestAdapterApp();
33+
const request = new Request('http://example.com/causes-error');
34+
const response = await app.render(request);
35+
expect(response.status).to.equal(500);
36+
const html = await response.text();
37+
const $ = cheerio.load(html);
38+
expect($('h1').text()).to.equal('This is an error page');
39+
});
40+
});

0 commit comments

Comments
 (0)