Skip to content

Commit

Permalink
Implement the feature
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewp committed Jul 22, 2022
1 parent a4f9abf commit de3d881
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 105 deletions.
8 changes: 8 additions & 0 deletions .changeset/happy-parrots-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'astro': minor
'@astrojs/cloudflare': minor
'@astrojs/netlify': minor
'@astrojs/vercel': minor
---

Support for 404 and 500 pages in SSR
48 changes: 41 additions & 7 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -46,17 +50,30 @@ 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,
Expand All @@ -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}].`);
}
Expand All @@ -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;
Expand Down Expand Up @@ -128,6 +159,7 @@ export class App {
ssr: true,
request,
streaming: this.#streaming,
status
});

return response;
Expand All @@ -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;
Expand All @@ -155,6 +188,7 @@ export class App {
route: routeData,
routeCache: this.#routeCache,
ssr: true,
status
});

if (result.type === 'response') {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/core/render/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface RenderOptions {
ssr: boolean;
streaming: boolean;
request: Request;
status?: number;
}

export async function render(opts: RenderOptions): Promise<Response> {
Expand All @@ -107,6 +108,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
site,
ssr,
streaming,
status = 200
} = opts;

const paramsAndPropsRes = await getParamsAndProps({
Expand Down Expand Up @@ -148,6 +150,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
scripts,
ssr,
streaming,
status
});

// Support `export const components` for `MDX` pages
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface CreateResultArgs {
scripts?: Set<SSRElement>;
styles?: Set<SSRElement>;
request: Request;
status: number;
}

function getFunctionExpression(slot: any) {
Expand Down Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
---
---

<h1>Something went horribly wrong!!</h1>
<h1>Something went horribly wrong!</h1>
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
---
throw new Error(`oops`);
---
<h1>This is an error page</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
throw new Error(`oops`);
---
40 changes: 40 additions & 0 deletions packages/astro/test/ssr-404-500-pages.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
114 changes: 38 additions & 76 deletions packages/astro/test/ssr-api-route.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';

describe('API routes in SSR', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let errorFixtures;

before(async () => {
fixture = await loadFixture({
Expand All @@ -16,97 +14,61 @@ describe('API routes in SSR', () => {
},
adapter: testAdapter(),
});
errorFixtures = await loadFixture({
root: './fixtures/ssr-api-route-custom-404/',
experimental: {
ssr: true,
},
server: {
port: 5173
},
adapter: testAdapter(),
});
await errorFixtures.build();
await fixture.build();
});

// it('Basic pages work', async () => {
// const app = await fixture.loadTestAdapterApp();
// const request = new Request('http://example.com/');
// const response = await app.render(request);
// const html = await response.text();
// expect(html).to.not.be.empty;
// });
it('Basic pages work', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
const html = await response.text();
expect(html).to.not.be.empty;
});

// it('Can load the API route too', async () => {
// const app = await fixture.loadTestAdapterApp();
// const request = new Request('http://example.com/food.json');
// const response = await app.render(request);
// expect(response.status).to.equal(200);
// expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
// expect(response.headers.get('Content-Length')).to.not.be.empty;
// const body = await response.json();
// expect(body.length).to.equal(3);
// });
it('Can load the API route too', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/food.json');
const response = await app.render(request);
expect(response.status).to.equal(200);
expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
expect(response.headers.get('Content-Length')).to.not.be.empty;
const body = await response.json();
expect(body.length).to.equal(3);
});

describe('API Routes - Dev', () => {
let devServer;
let errorDevServer;
before(async () => {
devServer = await fixture.startDevServer();
errorDevServer = await errorFixtures.startDevServer();
});

after(async () => {
await devServer.stop();
await errorDevServer.stop();
});

// it('Can POST to API routes', async () => {
// const response = await fixture.fetch('/food.json', {
// method: 'POST',
// body: `some data`,
// });
// expect(response.status).to.equal(200);
// const text = await response.text();
// expect(text).to.equal(`ok`);
// });

// it('Infer content type with charset for { body } shorthand', async () => {
// const response = await fixture.fetch('/food.json', {
// method: 'GET',
// });
// expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
// });

// it('Can set multiple headers of the same type', async () => {
// const response = await fixture.fetch('/login', {
// method: 'POST',
// });
// const setCookie = response.headers.get('set-cookie');
// expect(setCookie).to.equal('foo=foo; HttpOnly, bar=bar; HttpOnly');
// });

it('renders default 404 page for /404', async () => {
const html = await fixture.fetch('/404').then((res) => res.text());
const $ = cheerio.load(html);

expect($('h1').text()).to.equal('404: Not found');
// expect($('p').text()).to.equal('/a/');
it('Can POST to API routes', async () => {
const response = await fixture.fetch('/food.json', {
method: 'POST',
body: `some data`,
});
expect(response.status).to.equal(200);
const text = await response.text();
expect(text).to.equal(`ok`);
});

// it('renders custom 404 page for /a', async () => {
// const html = await errorFixtures.fetch('/a').then((res) => res.text());
// const $ = cheerio.load(html);

// expect($('h1').text()).to.equal('Something went horribly wrong!!');
// });

// it('500 page for /500', async () => {
// const html = await fixture.fetch('/500').then((res) => res.text());
// const $ = cheerio.load(html);
it('Infer content type with charset for { body } shorthand', async () => {
const response = await fixture.fetch('/food.json', {
method: 'GET',
});
expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
});

// expect($('title').text().length).to.equal('Something went horribly wrong!!');
// });
it('Can set multiple headers of the same type', async () => {
const response = await fixture.fetch('/login', {
method: 'POST',
});
const setCookie = response.headers.get('set-cookie');
expect(setCookie).to.equal('foo=foo; HttpOnly, bar=bar; HttpOnly');
});
});
});
11 changes: 3 additions & 8 deletions packages/integrations/cloudflare/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
Loading

0 comments on commit de3d881

Please sign in to comment.