Skip to content
Merged
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/witty-jobs-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes localized error pages such as `src/pages/pt/404.astro` returning a `200` status when rendered through the `pages()` handler from `astro/fetch` or `astro/hono`
25 changes: 23 additions & 2 deletions packages/astro/src/core/fetch/fetch-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import { MultiLevelEncodingError, validateAndDecodePathname } from '../util/path
import { setPathname } from '../util/normalized-url.js';
import { getOriginPathname, setOriginPathname } from '../routing/rewrite.js';
import { computePathnameFromDomain } from '../i18n/domain.js';
import { getCustom404Route, routeHasHtmlExtension } from '../routing/helpers.js';
import {
getCustom404Route,
getDefaultStatusCode,
routeHasHtmlExtension,
} from '../routing/helpers.js';
import type { RenderErrorOptions, ResolvedRenderOptions } from '../app/base.js';
import { getRenderOptions } from '../app/render-options.js';
import { getFirstForwardedValue, validateForwardedHeaders } from '../app/validate-headers.js';
Expand Down Expand Up @@ -221,12 +225,29 @@ export class FetchState implements AstroFetchState {
* rendering or middleware completes.
*/
response: Response | undefined;
/** Explicitly assigned status; `undefined` until a caller sets one. */
#status: number | undefined;
/**
* Default HTTP status for the rendered response. Callers override
* before rendering runs (e.g. `handleRequest` sets this from
* `BaseApp.getDefaultStatusCode`; error handlers set `404` / `500`).
* Reads `200` until assigned.
*/
status = 200;
get status(): number {
return this.#status ?? 200;
}
set status(value: number) {
this.#status = value;
}
/**
* Sets `status` from the matched route (see `getDefaultStatusCode`)
* unless a caller already assigned one.
*/
applyDefaultStatus(): void {
if (this.#status === undefined && this.routeData) {
this.#status = getDefaultStatusCode(this.manifest, this.routeData, this.pathname);
}
}
/** Whether user middleware should be skipped for this request. */
skipMiddleware = false;
/**
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/pages/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export async function handlePagesWithErrorFallback(state: FetchState): Promise<R
) {
return createCrossOriginForbiddenResponse(ctx.request);
}
state.applyDefaultStatus();
try {
return await handlePages(state, ctx);
} catch (err: any) {
Expand Down
65 changes: 65 additions & 0 deletions packages/astro/test/units/fetch/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,71 @@ describe('pages()', () => {
assert.match(text, /<h1>my custom 500<\/h1>/);
});

it('renders a localized 404 route with a 404 status', async () => {
const notFoundPage = createComponent((_result: any, _props: any, _slots: any) => {
return render`<h1>Não encontrado</h1>`;
});
const app = createTestApp(
[createPage(simplePage, { route: '/' }), createPage(notFoundPage, { route: '/pt/404' })],
{
i18n: {
defaultLocale: 'en',
locales: ['en', 'pt'],
strategy: 'manual',
fallbackType: 'rewrite',
fallback: {},
domains: {},
domainLookupTable: {},
},
},
);
const request = stampApp(new Request('http://example.com/pt/404'), app);
const state = new FetchState(request);

const response = await pages(state);

assert.equal(response.status, 404);
const text = await response.text();
assert.match(text, /<h1>Não encontrado<\/h1>/);
});

it('renders a localized 500 route with a 500 status', async () => {
const errorPage = createComponent((_result: any, _props: any, _slots: any) => {
return render`<h1>Erro</h1>`;
});
const app = createTestApp(
[createPage(simplePage, { route: '/' }), createPage(errorPage, { route: '/pt/500' })],
{
i18n: {
defaultLocale: 'en',
locales: ['en', 'pt'],
strategy: 'manual',
fallbackType: 'rewrite',
fallback: {},
domains: {},
domainLookupTable: {},
},
},
);
const request = stampApp(new Request('http://example.com/pt/500'), app);
const state = new FetchState(request);

const response = await pages(state);

assert.equal(response.status, 500);
});

it('honors a status set on the state before rendering', async () => {
const app = createTestApp([createPage(simplePage, { route: '/' })]);
const request = stampApp(new Request('http://example.com/'), app);
const state = new FetchState(request);
state.status = 503;

const response = await pages(state);

assert.equal(response.status, 503);
});

it('returns a marked 404 for the app post-check when the custom 404 route is prerendered', async () => {
const notFoundPage = createComponent((_result: any, _props: any, _slots: any) => {
return render`<h1>Not Found</h1>`;
Expand Down
Loading