Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-ravens-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: render the error page with the post-`handle` headers instead of failing on headers the crashed render already set
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export async function respond_with_error({ event, state, error, resolve_opts })
return static_error_page(transformed.status, transformed.message);
}

state.reset_headers?.();

/** @type {import('./types.js').Fetched[]} */
const fetched = [];
try {
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export async function internal_respond(request, state) {
}

/** @type {Record<string, string>} */
const headers = {};
let headers = {};

const { cookies, new_cookies, get_cookie_header, set_internal, set_trailing_slash } = get_cookies(
request,
Expand Down Expand Up @@ -583,6 +583,9 @@ export async function internal_respond(request, state) {
* @param {import('@sveltejs/kit/hooks').ResolveOptions} [opts]
*/
async function resolve(event, page_nodes, opts) {
const handle_headers = { ...headers };
state.reset_headers ??= () => (headers = { ...handle_headers });

try {
if (opts) {
resolve_opts = {
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,8 @@ export type RecordSpan = <T>(options: {
* used for tracking things like remote function calls
*/
export interface RequestState {
/** discards headers set by a render that failed, so the error page starts from the post-`handle` state */
reset_headers?(): void;
readonly getClientAddress: () => string;
readonly platform?: any;
/** @internal reads from the filesystem when user code tries to fetch a static asset */
Expand Down
14 changes: 12 additions & 2 deletions packages/kit/test/apps/basics/src/hooks.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,14 @@ export const handle = sequence(
const response = await resolve(event, {
transformPageChunk: event.url.pathname.startsWith('/transform-page-chunk')
? ({ html }) => html.replace('__REPLACEME__', 'Worked!')
: undefined
: event.url.pathname.startsWith('/errors/error-page-setheaders')
? ({ html }) => {
if (html.includes('makes the page transform crash')) {
throw new Error('Crashing now');
}
return html;
}
: undefined
});

try {
Expand Down Expand Up @@ -192,7 +199,10 @@ export const handle = sequence(
return resolve(event);
},
async ({ event, resolve }) => {
if (['/non-existent-route', '/non-existent-route-loop'].includes(event.url.pathname)) {
if (
event.url.pathname.startsWith('/errors/error-page-setheaders') ||
['/non-existent-route', '/non-existent-route-loop'].includes(event.url.pathname)
) {
event.locals.url = new URL(event.request.url);
}
return resolve(event);
Expand Down
6 changes: 5 additions & 1 deletion packages/kit/test/apps/basics/src/routes/+layout.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ if (JSON.parse(SOME_JSON).answer !== 42) {
}

/** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies, locals, fetch }) {
export async function load({ cookies, locals, fetch, setHeaders }) {
if (locals.url?.pathname.startsWith('/errors/error-page-setheaders')) {
setHeaders({ 'cache-control': 'private, max-age=60' });
}

if (locals.url?.pathname === '/non-existent-route') {
await fetch('/prerendering/prerendered-endpoint/api').then((r) => r.json());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('./$types').PageServerLoad} */
export function load({ setHeaders }) {
setHeaders({ 'x-failed-render': '1' });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>this text makes the page transform crash</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>this text makes the page transform crash</p>
17 changes: 17 additions & 0 deletions packages/kit/test/apps/basics/test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,23 @@ test.describe('setHeaders', () => {
expect(cookies).toMatch('cookie1=value1');
expect(cookies).toMatch('cookie2=value2');
});

test('renders the error page when the root layout sets headers', async ({ request }) => {
const response = await request.get('/errors/error-page-setheaders');

expect(response.status()).toBe(500);
expect(response.headers()['cache-control']).toBe('private, max-age=60');
expect(await response.text()).toContain('This is your custom error page saying:');
});

test('drops headers set by the failed render from the error page', async ({ request }) => {
const response = await request.get('/errors/error-page-setheaders-leak');

expect(response.status()).toBe(500);
expect(response.headers()['x-failed-render']).toBeUndefined();
expect(response.headers()['cache-control']).toBe('private, max-age=60');
expect(await response.text()).toContain('This is your custom error page saying:');
});
});

test.describe('cookies', () => {
Expand Down
Loading