Skip to content

Commit d07d2f7

Browse files
ematipicoflorian-lefebvre
andauthoredJun 13, 2024··
fix: better DX for 500.astro in local development (#11244)
* wip * catch error in route.ts * catch error in route.ts * chore: tidy up error cases * log the original error * rebase * chore: reduce the scope of the 500 handling * we should not have a default 500 * remove props * remove unsure function, not needed * Update packages/astro/src/core/routing/astro-designed-error-pages.ts Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * Update packages/astro/src/core/constants.ts Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * changeset * relax the assertion * Update packages/astro/src/vite-plugin-astro-server/route.ts Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * relax the assertion --------- Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
1 parent 8036b9a commit d07d2f7

File tree

12 files changed

+163
-8
lines changed

12 files changed

+163
-8
lines changed
 

‎.changeset/olive-feet-eat.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Improves the developer experience of the custom `500.astro` page in development mode.
6+
7+
Before, in development, an error thrown during the rendering phase would display the default error overlay, even when users had the `500.astro` page.
8+
9+
Now, the development server will display the `500.astro` and the original error is logged in the console.

‎packages/astro/src/core/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export const ROUTE_TYPE_HEADER = 'X-Astro-Route-Type';
3232
*/
3333
export const DEFAULT_404_COMPONENT = 'astro-default-404.astro';
3434

35+
/**
36+
* The value of the `component` field of the default 500 page, which is used when there is no user-provided 404.astro page.
37+
*/
38+
export const DEFAULT_500_COMPONENT = 'astro-default-500.astro';
39+
3540
/**
3641
* A response with one of these status codes will be rewritten
3742
* with the result of rendering the respective error page.

‎packages/astro/src/core/routing/astro-designed-error-pages.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ManifestData, RouteData } from '../../@types/astro.js';
22
import notFoundTemplate from '../../template/4xx.js';
3-
import { DEFAULT_404_COMPONENT } from '../constants.js';
3+
import { DEFAULT_404_COMPONENT, DEFAULT_500_COMPONENT } from '../constants.js';
44

55
export const DEFAULT_404_ROUTE: RouteData = {
66
component: DEFAULT_404_COMPONENT,
@@ -16,6 +16,20 @@ export const DEFAULT_404_ROUTE: RouteData = {
1616
isIndex: false,
1717
};
1818

19+
export const DEFAULT_500_ROUTE: RouteData = {
20+
component: DEFAULT_500_COMPONENT,
21+
generate: () => '',
22+
params: [],
23+
pattern: /\/500/,
24+
prerender: false,
25+
pathname: '/500',
26+
segments: [[{ content: '500', dynamic: false, spread: false }]],
27+
type: 'page',
28+
route: '/500',
29+
fallbackRoutes: [],
30+
isIndex: false,
31+
};
32+
1933
export function ensure404Route(manifest: ManifestData) {
2034
if (!manifest.routes.some((route) => route.route === '/404')) {
2135
manifest.routes.push(DEFAULT_404_ROUTE);

‎packages/astro/src/vite-plugin-astro-server/route.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ function getCustom404Route(manifestData: ManifestData): RouteData | undefined {
4141
return manifestData.routes.find((r) => route404.test(r.route));
4242
}
4343

44+
function getCustom500Route(manifestData: ManifestData): RouteData | undefined {
45+
const route500 = /^\/500\/?$/;
46+
return manifestData.routes.find((r) => route500.test(r.route));
47+
}
48+
4449
export async function matchRoute(
4550
pathname: string,
4651
manifestData: ManifestData,
@@ -273,7 +278,22 @@ export async function handleRoute({
273278
});
274279
}
275280

276-
let response = await renderContext.render(mod);
281+
let response;
282+
try {
283+
response = await renderContext.render(mod);
284+
} catch (err: any) {
285+
const custom500 = getCustom500Route(manifestData);
286+
if (!custom500) {
287+
throw err;
288+
}
289+
// Log useful information that the custom 500 page may not display unlike the default error overlay
290+
logger.error('router', err.stack || err.message);
291+
const filePath = new URL(`./${custom500.component}`, config.root);
292+
const preloadedComponent = await pipeline.preload(custom500, filePath);
293+
response = await renderContext.render(preloadedComponent);
294+
status = 500;
295+
}
296+
277297
if (isLoggedRequest(pathname)) {
278298
const timeEnd = performance.now();
279299
logger.info(
@@ -321,6 +341,7 @@ export async function handleRoute({
321341
await writeSSRResult(request, response, incomingResponse);
322342
return;
323343
}
344+
324345
// Apply the `status` override to the response object before responding.
325346
// Response.status is read-only, so a clone is required to override.
326347
if (status && response.status !== status && (status === 404 || status === 500)) {

‎packages/astro/test/core-image.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ describe('astro:image', () => {
712712
let res = await fixture.fetch('/get-image-empty');
713713
await res.text();
714714

715-
assert.equal(logs.length, 1);
715+
assert.equal(logs.length >= 1, true);
716716
assert.equal(logs[0].message.includes('Expected getImage() parameter'), true);
717717
});
718718

@@ -721,7 +721,7 @@ describe('astro:image', () => {
721721
let res = await fixture.fetch('/get-image-undefined');
722722
await res.text();
723723

724-
assert.equal(logs.length, 1);
724+
assert.equal(logs.length >= 1, true);
725725
assert.equal(logs[0].message.includes('Expected `src` property'), true);
726726
});
727727

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from 'astro/config';
2+
3+
// https://astro.build/config
4+
export default defineConfig({
5+
experimental: {
6+
rewriting: true
7+
},
8+
site: "https://example.com"
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@test/rewrite-runtime-errror-custom500",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
<h1>I am the custom 500</h1>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
return Astro.rewrite("/errors/throw")
3+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
throw new Error("Fancy error")
3+
---

‎packages/astro/test/rewrite.test.js

+77-4
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ describe('Middleware', () => {
314314
});
315315
});
316316

317-
describe('Runtime error', () => {
317+
describe('Runtime error, default 500', () => {
318318
/** @type {import('./test-utils').Fixture} */
319319
let fixture;
320320
let devServer;
@@ -330,10 +330,83 @@ describe('Runtime error', () => {
330330
await devServer.stop();
331331
});
332332

333-
it('should render the index page when navigating /reroute ', async () => {
334-
const html = await fixture.fetch('/errors/from').then((res) => res.text());
333+
it('should return a 500 status code, but not render the custom 500', async () => {
334+
const response = await fixture.fetch('/errors/from');
335+
assert.equal(response.status, 500);
336+
const text = await response.text();
337+
assert.match(text, /@vite\/client/);
338+
});
339+
});
340+
341+
describe('Runtime error in SSR, default 500', () => {
342+
/** @type {import('./test-utils').Fixture} */
343+
let fixture;
344+
let app;
345+
346+
before(async () => {
347+
fixture = await loadFixture({
348+
root: './fixtures/rewrite-runtime-error/',
349+
output: 'server',
350+
adapter: testAdapter(),
351+
});
352+
await fixture.build();
353+
app = await fixture.loadTestAdapterApp();
354+
});
355+
356+
it('should return a 500 status code, but not render the custom 500', async () => {
357+
const request = new Request('http://example.com/errors/from');
358+
const response = await app.render(request);
359+
const text = await response.text();
360+
assert.equal(text, '');
361+
});
362+
});
363+
364+
describe('Runtime error in dev, custom 500', () => {
365+
/** @type {import('./test-utils').Fixture} */
366+
let fixture;
367+
let devServer;
368+
369+
before(async () => {
370+
fixture = await loadFixture({
371+
root: './fixtures/rewrite-runtime-error-custom500/',
372+
});
373+
devServer = await fixture.startDevServer();
374+
});
375+
376+
after(async () => {
377+
await devServer.stop();
378+
});
379+
380+
it('should render the custom 500 when rewriting a page that throws an error', async () => {
381+
const response = await fixture.fetch('/errors/start');
382+
assert.equal(response.status, 500);
383+
const html = await response.text();
384+
assert.match(html, /I am the custom 500/);
385+
});
386+
});
387+
388+
describe('Runtime error in SSR, custom 500', () => {
389+
/** @type {import('./test-utils').Fixture} */
390+
let fixture;
391+
let app;
392+
393+
before(async () => {
394+
fixture = await loadFixture({
395+
root: './fixtures/rewrite-runtime-error-custom500/',
396+
output: 'server',
397+
adapter: testAdapter(),
398+
});
399+
await fixture.build();
400+
app = await fixture.loadTestAdapterApp();
401+
});
402+
403+
it('should render the custom 500 when rewriting a page that throws an error', async () => {
404+
const request = new Request('http://example.com/errors/start');
405+
const response = await app.render(request);
406+
const html = await response.text();
407+
335408
const $ = cheerioLoad(html);
336409

337-
assert.equal($('title').text(), 'Error');
410+
assert.equal($('h1').text(), 'I am the custom 500');
338411
});
339412
});

‎pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.