Skip to content

Commit

Permalink
feat: 500.astro improvements (#11134)
Browse files Browse the repository at this point in the history
* feat: better error handling

* feat: allow passing props to render context render

* feat: work on tests

* Update 500.astro

* feat: test preview custom 500

* feat: test for custom 500 failing

* feat: add changeset

* Update rich-dolls-compete.md

* Delete packages/astro/e2e/custom-500.test.js

* Apply suggestions from code review

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* fix: merge

* Update packages/astro/test/custom-500.test.js

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

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

* feat: update

---------

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
  • Loading branch information
4 people authored Jun 19, 2024
1 parent c44f7f4 commit 9042be0
Show file tree
Hide file tree
Showing 15 changed files with 270 additions and 10 deletions.
22 changes: 22 additions & 0 deletions .changeset/rich-dolls-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"astro": minor
---

Improves the developer experience of the `500.astro` file by passing it a new `error` prop.

When an error is thrown, the special `src/pages/500.astro` page now automatically receives the error as a prop. This allows you to display more specific information about the error on a custom 500 page.

```astro
---
// src/pages/500.astro
interface Props {
error: unknown
}
const { error } = Astro.props
---
<div>{error instanceof Error ? error.message : 'Unknown error'}</div>
```

If an error occurs rendering this page, your host's default 500 error page will be shown to your visitor in production, and Astro's default error overlay will be shown in development.
7 changes: 3 additions & 4 deletions packages/astro/playwright.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defineConfig } from '@playwright/test';
// NOTE: Sometimes, tests fail with `TypeError: process.stdout.clearLine is not a function`
// for some reason. This comes from Vite, and is conditionally called based on `isTTY`.
// We set it to false here to skip this odd behavior.
process.stdout.isTTY = false;

const config = {
export default defineConfig({
testMatch: 'e2e/*.test.js',
/* Maximum time one test can run for. */
timeout: 40 * 1000,
Expand Down Expand Up @@ -37,6 +38,4 @@ const config = {
},
},
],
};

export default config;
});
23 changes: 19 additions & 4 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export interface RenderErrorOptions {
* Whether to skip middleware while rendering the error page. Defaults to false.
*/
skipMiddleware?: boolean;
/**
* Allows passing an error to 500.astro. It will be available through `Astro.props.error`.
*/
error?: unknown;
}

export class App {
Expand Down Expand Up @@ -289,8 +293,9 @@ export class App {
}
if (locals) {
if (typeof locals !== 'object') {
this.#logger.error(null, new AstroError(AstroErrorData.LocalsNotAnObject).stack!);
return this.#renderError(request, { status: 500 });
const error = new AstroError(AstroErrorData.LocalsNotAnObject);
this.#logger.error(null, error.stack!);
return this.#renderError(request, { status: 500, error });
}
Reflect.set(request, clientLocalsSymbol, locals);
}
Expand Down Expand Up @@ -324,7 +329,7 @@ export class App {
response = await renderContext.render(await mod.page());
} catch (err: any) {
this.#logger.error(null, err.stack || err.message || String(err));
return this.#renderError(request, { locals, status: 500 });
return this.#renderError(request, { locals, status: 500, error: err });
}

if (
Expand All @@ -335,6 +340,9 @@ export class App {
locals,
response,
status: response.status as 404 | 500,
// We don't have an error to report here. Passing null means we pass nothing intentionally
// while undefined means there's no error
error: response.status === 500 ? null : undefined,
});
}

Expand Down Expand Up @@ -385,7 +393,13 @@ export class App {
*/
async #renderError(
request: Request,
{ locals, status, response: originalResponse, skipMiddleware = false }: RenderErrorOptions
{
locals,
status,
response: originalResponse,
skipMiddleware = false,
error,
}: RenderErrorOptions
): Promise<Response> {
const errorRoutePath = `/${status}${this.#manifest.trailingSlash === 'always' ? '/' : ''}`;
const errorRouteData = matchRoute(errorRoutePath, this.#manifestData);
Expand Down Expand Up @@ -415,6 +429,7 @@ export class App {
request,
routeData: errorRouteData,
status,
props: { error }
});
const response = await renderContext.render(await mod.page());
return this.#mergeResponses(response, originalResponse);
Expand Down
9 changes: 7 additions & 2 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,21 @@ export class RenderContext {
request,
routeData,
status = 200,
props,
}: Pick<RenderContext, 'pathname' | 'pipeline' | 'request' | 'routeData'> &
Partial<Pick<RenderContext, 'locals' | 'middleware' | 'status'>>): RenderContext {
Partial<Pick<RenderContext, 'locals' | 'middleware' | 'status' | 'props'>>): RenderContext {
return new RenderContext(
pipeline,
locals,
sequence(...pipeline.internalMiddleware, middleware ?? pipeline.middleware),
pathname,
request,
routeData,
status
status,
undefined,
undefined,
undefined,
props
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/vite-plugin-astro-server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export async function handleRoute({
logger.error('router', err.stack || err.message);
const filePath500 = new URL(`./${custom500.component}`, config.root);
const preloaded500Component = await pipeline.preload(custom500, filePath500);
renderContext.props.error = err;
response = await renderContext.render(preloaded500Component);
status = 500;
}
Expand Down
122 changes: 122 additions & 0 deletions packages/astro/test/custom-500.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import assert from 'node:assert/strict';
import { afterEach, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';
import { renameSync } from 'node:fs';

describe('Custom 500', () => {
/** @type {Awaited<ReturnType<typeof loadFixture>>} */
let fixture;

describe('dev', () => {
/** @type {Awaited<ReturnType<(typeof fixture)["startDevServer"]>>} */
let devServer;

afterEach(async () => {
await devServer?.stop();
try {
renameSync(
new URL('./fixtures/custom-500/src/pages/_500.astro', import.meta.url),
new URL('./fixtures/custom-500/src/pages/500.astro', import.meta.url)
);
} catch (_) {}
});

it('renders default error overlay', async () => {
renameSync(
new URL('./fixtures/custom-500/src/pages/500.astro', import.meta.url),
new URL('./fixtures/custom-500/src/pages/_500.astro', import.meta.url)
);
fixture = await loadFixture({
root: './fixtures/custom-500/',
output: 'server',
adapter: testAdapter(),
});
devServer = await fixture.startDevServer();

const response = await fixture.fetch('/');
assert.equal(response.status, 500);

const html = await response.text();

assert.equal(html, '<title>Error</title><script type="module" src="/@vite/client"></script>');
});

it('renders custom 500', async () => {
fixture = await loadFixture({
root: './fixtures/custom-500/',
output: 'server',
adapter: testAdapter(),
});
devServer = await fixture.startDevServer();

const response = await fixture.fetch('/');
assert.equal(response.status, 500);

const html = await response.text();
const $ = cheerio.load(html);

assert.equal($('h1').text(), 'Server error');
assert.equal($('p').text(), 'some error');
});

it('renders default error overlay if custom 500 throws', async () => {
fixture = await loadFixture({
root: './fixtures/custom-500-failing/',
output: 'server',
adapter: testAdapter(),
});
devServer = await fixture.startDevServer();

const response = await fixture.fetch('/');
assert.equal(response.status, 500);

const html = await response.text();

assert.equal(html, '<title>Error</title><script type="module" src="/@vite/client"></script>');
});
});

describe('SSR', () => {
/** @type {Awaited<ReturnType<(typeof fixture)["loadTestAdapterApp"]>>} */
let app;

it('renders custom 500', async () => {
fixture = await loadFixture({
root: './fixtures/custom-500/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
app = await fixture.loadTestAdapterApp();

const request = new Request('http://example.com/');
const response = await app.render(request);
assert.equal(response.status, 500);

const html = await response.text();
const $ = cheerio.load(html);

assert.equal($('h1').text(), 'Server error');
assert.equal($('p').text(), 'some error');
});

it('renders nothing if custom 500 throws', async () => {
fixture = await loadFixture({
root: './fixtures/custom-500-failing/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
app = await fixture.loadTestAdapterApp();

const request = new Request('http://example.com/');
const response = await app.render(request);
assert.equal(response.status, 500);

const html = await response.text();
assert.equal(html, '');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({});
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/custom-500-failing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/custom-500-failing",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
interface Props {
error: unknown
}
const { error } = Astro.props
throw "custom 500 fail"
---

<html lang="en">
<head>
<title>Server error - Custom 500</title>
</head>
<body>
<h1>Server error</h1>
<p>{error}</p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
throw "some error"
---

<html lang="en">
<head>
<title>Custom 500</title>
</head>
<body>
<h1>Home</h1>
</body>
</html>
4 changes: 4 additions & 0 deletions packages/astro/test/fixtures/custom-500/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({});
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/custom-500/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/custom-500",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
17 changes: 17 additions & 0 deletions packages/astro/test/fixtures/custom-500/src/pages/500.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
interface Props {
error: unknown
}
const { error } = Astro.props
---

<html lang="en">
<head>
<title>Server error - Custom 500</title>
</head>
<body>
<h1>Server error</h1>
<p>{error}</p>
</body>
</html>
12 changes: 12 additions & 0 deletions packages/astro/test/fixtures/custom-500/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
throw "some error"
---

<html lang="en">
<head>
<title>Custom 500</title>
</head>
<body>
<h1>Home</h1>
</body>
</html>
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9042be0

Please sign in to comment.