summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Okiki Ojo <okikio.dev@gmail.com> 2022-07-22 16:30:17 -0400
committerGravatar GitHub <noreply@github.com> 2022-07-22 16:30:17 -0400
commit0cc6ede362996b9faba57481a790d6eb7fba2045 (patch)
treeb2f5894c8adb2e28adf6e2fcd8ee3a46bad6b189
parent4392083ccad7f94083f99a37370fa2d01cffba80 (diff)
downloadastro-0cc6ede362996b9faba57481a790d6eb7fba2045.tar.gz
astro-0cc6ede362996b9faba57481a790d6eb7fba2045.tar.zst
astro-0cc6ede362996b9faba57481a790d6eb7fba2045.zip
SSR 404 and 500 routes in adapters (#4018)
* fix(WIP): SSR 404 and 500 routes * Implement the feature Co-authored-by: Matthew Phillips <matthew@skypack.dev>
-rw-r--r--.changeset/happy-parrots-stare.md8
-rw-r--r--packages/astro/src/core/app/index.ts48
-rw-r--r--packages/astro/src/core/endpoint/index.ts2
-rw-r--r--packages/astro/src/core/render/core.ts3
-rw-r--r--packages/astro/src/core/render/result.ts3
-rw-r--r--packages/astro/test/fixtures/ssr-api-route-custom-404/package.json8
-rw-r--r--packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro1
-rw-r--r--packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro1
-rw-r--r--packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-error.astro3
-rw-r--r--packages/astro/test/ssr-404-500-pages.test.js40
-rw-r--r--packages/integrations/cloudflare/src/server.ts11
-rw-r--r--packages/integrations/netlify/src/netlify-functions.ts6
-rw-r--r--packages/integrations/vercel/src/serverless/entrypoint.ts5
-rw-r--r--pnpm-lock.yaml6
14 files changed, 124 insertions, 21 deletions
diff --git a/.changeset/happy-parrots-stare.md b/.changeset/happy-parrots-stare.md
new file mode 100644
index 000000000..54d117320
--- /dev/null
+++ b/.changeset/happy-parrots-stare.md
@@ -0,0 +1,8 @@
+---
+'astro': minor
+'@astrojs/cloudflare': minor
+'@astrojs/netlify': minor
+'@astrojs/vercel': minor
+---
+
+Support for 404 and 500 pages in SSR
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 518c1fc58..7adde8820 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -25,6 +25,10 @@ export { deserializeManifest } from './common.js';
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
+export interface MatchOptions {
+ matchNotFound?: boolean | undefined;
+}
+
export class App {
#manifest: Manifest;
#manifestData: ManifestData;
@@ -46,18 +50,31 @@ export class App {
this.#routeCache = new RouteCache(this.#logging);
this.#streaming = streaming;
}
- match(request: Request): RouteData | undefined {
+ match(request: Request, { matchNotFound = false }: MatchOptions = {}): RouteData | undefined {
const url = new URL(request.url);
// ignore requests matching public assets
if (this.#manifest.assets.has(url.pathname)) {
return undefined;
}
- return matchRoute(url.pathname, this.#manifestData);
+ let routeData = matchRoute(url.pathname, this.#manifestData);
+
+ if(routeData) {
+ return routeData;
+ } else if(matchNotFound) {
+ return matchRoute('/404', this.#manifestData);
+ } else {
+ return undefined;
+ }
}
async render(request: Request, routeData?: RouteData): Promise<Response> {
+ let defaultStatus = 200;
if (!routeData) {
routeData = this.match(request);
if (!routeData) {
+ defaultStatus = 404;
+ routeData = this.match(request, { matchNotFound: true });
+ }
+ if (!routeData) {
return new Response(null, {
status: 404,
statusText: 'Not found',
@@ -65,12 +82,25 @@ export class App {
}
}
- const mod = this.#manifest.pageMap.get(routeData.component)!;
+ let mod = this.#manifest.pageMap.get(routeData.component)!;
if (routeData.type === 'page') {
- return this.#renderPage(request, routeData, mod);
+ let response = await this.#renderPage(request, routeData, mod, defaultStatus);
+
+ // If there was a 500 error, try sending the 500 page.
+ if(response.status === 500) {
+ const fiveHundredRouteData = matchRoute('/500', this.#manifestData);
+ if(fiveHundredRouteData) {
+ mod = this.#manifest.pageMap.get(fiveHundredRouteData.component)!;
+ try {
+ let fiveHundredResponse = await this.#renderPage(request, fiveHundredRouteData, mod, 500);
+ return fiveHundredResponse;
+ } catch {}
+ }
+ }
+ return response;
} else if (routeData.type === 'endpoint') {
- return this.#callEndpoint(request, routeData, mod);
+ return this.#callEndpoint(request, routeData, mod, defaultStatus);
} else {
throw new Error(`Unsupported route type [${routeData.type}].`);
}
@@ -79,7 +109,8 @@ export class App {
async #renderPage(
request: Request,
routeData: RouteData,
- mod: ComponentInstance
+ mod: ComponentInstance,
+ status = 200
): Promise<Response> {
const url = new URL(request.url);
const manifest = this.#manifest;
@@ -128,6 +159,7 @@ export class App {
ssr: true,
request,
streaming: this.#streaming,
+ status
});
return response;
@@ -143,7 +175,8 @@ export class App {
async #callEndpoint(
request: Request,
routeData: RouteData,
- mod: ComponentInstance
+ mod: ComponentInstance,
+ status = 200
): Promise<Response> {
const url = new URL(request.url);
const handler = mod as unknown as EndpointHandler;
@@ -155,6 +188,7 @@ export class App {
route: routeData,
routeCache: this.#routeCache,
ssr: true,
+ status
});
if (result.type === 'response') {
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index 2e0318d58..117953dac 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -5,7 +5,7 @@ import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
export type EndpointOptions = Pick<
RenderOptions,
- 'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr'
+ 'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr' | 'status'
>;
type EndpointCallResult =
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index df8cb49d2..94aa62ac8 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -85,6 +85,7 @@ export interface RenderOptions {
ssr: boolean;
streaming: boolean;
request: Request;
+ status?: number;
}
export async function render(opts: RenderOptions): Promise<Response> {
@@ -107,6 +108,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
site,
ssr,
streaming,
+ status = 200
} = opts;
const paramsAndPropsRes = await getParamsAndProps({
@@ -148,6 +150,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
scripts,
ssr,
streaming,
+ status
});
// Support `export const components` for `MDX` pages
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index a7f36ee79..6e5da1d69 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -42,6 +42,7 @@ export interface CreateResultArgs {
scripts?: Set<SSRElement>;
styles?: Set<SSRElement>;
request: Request;
+ status: number;
}
function getFunctionExpression(slot: any) {
@@ -119,7 +120,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
headers.set('Content-Type', 'text/html');
}
const response: ResponseInit = {
- status: 200,
+ status: args.status,
statusText: 'OK',
headers,
};
diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/package.json b/packages/astro/test/fixtures/ssr-api-route-custom-404/package.json
new file mode 100644
index 000000000..cebb0cbaf
--- /dev/null
+++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/ssr-api-route-custom-404",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro
new file mode 100644
index 000000000..71a4a4d2c
--- /dev/null
+++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/404.astro
@@ -0,0 +1 @@
+<h1>Something went horribly wrong!</h1>
diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro
new file mode 100644
index 000000000..0e36085e2
--- /dev/null
+++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/500.astro
@@ -0,0 +1 @@
+<h1>This is an error page</h1>
diff --git a/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-error.astro b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-error.astro
new file mode 100644
index 000000000..28a3b7cc2
--- /dev/null
+++ b/packages/astro/test/fixtures/ssr-api-route-custom-404/src/pages/causes-error.astro
@@ -0,0 +1,3 @@
+---
+throw new Error(`oops`);
+---
diff --git a/packages/astro/test/ssr-404-500-pages.test.js b/packages/astro/test/ssr-404-500-pages.test.js
new file mode 100644
index 000000000..45d60d4ba
--- /dev/null
+++ b/packages/astro/test/ssr-404-500-pages.test.js
@@ -0,0 +1,40 @@
+import { expect } from 'chai';
+import { loadFixture } from './test-utils.js';
+import testAdapter from './test-adapter.js';
+import * as cheerio from 'cheerio';
+
+describe('404 and 500 pages', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/ssr-api-route-custom-404/',
+ experimental: {
+ ssr: true,
+ },
+ adapter: testAdapter(),
+ });
+ await fixture.build({ });
+ });
+
+ it('404 page returned when a route does not match', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/some/fake/route');
+ const response = await app.render(request);
+ expect(response.status).to.equal(404);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('h1').text()).to.equal('Something went horribly wrong!');
+ });
+
+ it('500 page returned when there is an error', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/causes-error');
+ const response = await app.render(request);
+ expect(response.status).to.equal(500);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('h1').text()).to.equal('This is an error page');
+ });
+});
diff --git a/packages/integrations/cloudflare/src/server.ts b/packages/integrations/cloudflare/src/server.ts
index 097f29d37..7b88c7b1e 100644
--- a/packages/integrations/cloudflare/src/server.ts
+++ b/packages/integrations/cloudflare/src/server.ts
@@ -19,19 +19,14 @@ export function createExports(manifest: SSRManifest) {
return env.ASSETS.fetch(assetRequest);
}
- if (app.match(request)) {
+ let routeData = app.match(request, { matchNotFound: true });
+ if (routeData) {
Reflect.set(
request,
Symbol.for('astro.clientAddress'),
request.headers.get('cf-connecting-ip')
);
- return app.render(request);
- }
-
- // 404
- const _404Request = new Request(`${origin}/404`, request);
- if (app.match(_404Request)) {
- return app.render(_404Request);
+ return app.render(request, routeData);
}
return new Response(null, {
diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts
index 0363fb803..d40254f96 100644
--- a/packages/integrations/netlify/src/netlify-functions.ts
+++ b/packages/integrations/netlify/src/netlify-functions.ts
@@ -66,7 +66,9 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
}
const request = new Request(rawUrl, init);
- if (!app.match(request)) {
+ let routeData = app.match(request, { matchNotFound: true });
+
+ if (!routeData) {
return {
statusCode: 404,
body: 'Not found',
@@ -76,7 +78,7 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
const ip = headers['x-nf-client-connection-ip'];
Reflect.set(request, clientAddressSymbol, ip);
- const response: Response = await app.render(request);
+ const response: Response = await app.render(request, routeData);
const responseHeaders = Object.fromEntries(response.headers.entries());
const responseContentType = parseContentType(responseHeaders['content-type']);
diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts
index 852aebefd..6b94f201c 100644
--- a/packages/integrations/vercel/src/serverless/entrypoint.ts
+++ b/packages/integrations/vercel/src/serverless/entrypoint.ts
@@ -22,12 +22,13 @@ export const createExports = (manifest: SSRManifest) => {
return res.end(err.reason || 'Invalid request body');
}
- if (!app.match(request)) {
+ let routeData = app.match(request, { matchNotFound: true });
+ if (!routeData) {
res.statusCode = 404;
return res.end('Not found');
}
- await setResponse(res, await app.render(request));
+ await setResponse(res, await app.render(request, routeData));
};
return { default: handler };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b728d2139..f6611b3bf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1770,6 +1770,12 @@ importers:
dependencies:
astro: link:../../..
+ packages/astro/test/fixtures/ssr-api-route-custom-404:
+ specifiers:
+ astro: workspace:*
+ dependencies:
+ astro: link:../../..
+
packages/astro/test/fixtures/ssr-assets:
specifiers:
astro: workspace:*