mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
feat: 500.astro improvements (#11134)
* 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>
This commit is contained in:
parent
c44f7f4bab
commit
9042be0491
15 changed files with 270 additions and 10 deletions
22
.changeset/rich-dolls-compete.md
Normal file
22
.changeset/rich-dolls-compete.md
Normal file
|
@ -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.
|
|
@ -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,
|
||||
|
@ -37,6 +38,4 @@ const config = {
|
|||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
|
|
@ -74,8 +74,9 @@ 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,
|
||||
|
@ -83,7 +84,11 @@ export class RenderContext {
|
|||
pathname,
|
||||
request,
|
||||
routeData,
|
||||
status
|
||||
status,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
props
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
122
packages/astro/test/custom-500.test.js
Normal file
122
packages/astro/test/custom-500.test.js
Normal file
|
@ -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, '');
|
||||
});
|
||||
});
|
||||
});
|
4
packages/astro/test/fixtures/custom-500-failing/astro.config.mjs
vendored
Normal file
4
packages/astro/test/fixtures/custom-500-failing/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
8
packages/astro/test/fixtures/custom-500-failing/package.json
vendored
Normal file
8
packages/astro/test/fixtures/custom-500-failing/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/custom-500-failing",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
19
packages/astro/test/fixtures/custom-500-failing/src/pages/500.astro
vendored
Normal file
19
packages/astro/test/fixtures/custom-500-failing/src/pages/500.astro
vendored
Normal file
|
@ -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>
|
12
packages/astro/test/fixtures/custom-500-failing/src/pages/index.astro
vendored
Normal file
12
packages/astro/test/fixtures/custom-500-failing/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
throw "some error"
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Custom 500</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Home</h1>
|
||||
</body>
|
||||
</html>
|
4
packages/astro/test/fixtures/custom-500/astro.config.mjs
vendored
Normal file
4
packages/astro/test/fixtures/custom-500/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
8
packages/astro/test/fixtures/custom-500/package.json
vendored
Normal file
8
packages/astro/test/fixtures/custom-500/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/custom-500",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
17
packages/astro/test/fixtures/custom-500/src/pages/500.astro
vendored
Normal file
17
packages/astro/test/fixtures/custom-500/src/pages/500.astro
vendored
Normal file
|
@ -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
packages/astro/test/fixtures/custom-500/src/pages/index.astro
vendored
Normal file
12
packages/astro/test/fixtures/custom-500/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
throw "some error"
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Custom 500</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Home</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -2895,6 +2895,18 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/custom-500:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/custom-500-failing:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/custom-assets-name:
|
||||
dependencies:
|
||||
'@astrojs/node':
|
||||
|
|
Loading…
Reference in a new issue