Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Proposal: server error middleware #1641

Closed
wants to merge 3 commits into from
Closed
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
29 changes: 17 additions & 12 deletions runtime/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,35 @@ declare module '@sapper/server' {
export type Ignore = string | RegExp | ((uri: string) => boolean) | Ignore[];

/**
* The request object passed to middleware and server-side routes.
* These fields are common to both Polka and Express, but you are free to
* The request object passed to middleware and server-side routes.
* These fields are common to both Polka and Express, but you are free to
* instead use the typings that come with the server you use.
*/
export interface SapperRequest extends IncomingMessage {
url: string;
method: string;
method: string;
baseUrl: string;

/**
* The originally requested URL, including parent router segments.
*/
originalUrl: string;

/**
* The path portion of the requested URL.
*/
path: string;

/**
* The values of named parameters within your route pattern
*/
params: Record<string, string>;

/**
* The un-parsed querystring
*/
search: string | null;

/**
* The parsed querystring
*/
Expand All @@ -75,15 +75,21 @@ declare module '@sapper/server' {
name?: string;
};
}


export type SapperNext = (err?: any) => void;

export type SapperHandler = (req: SapperRequest, res: SapperResponse, next: SapperNext) => void;

export type SapperErrorHandler = (err?: any, req: SapperRequest, res: SapperResponse, next: SapperNext) => void;

export interface MiddlewareOptions {
session?: (req: SapperRequest, res: SapperResponse) => unknown;
ignore?: Ignore;
}

export function middleware(
opts?: MiddlewareOptions
): (req: SapperRequest, res: SapperResponse, next: () => void) => void;
): SapperHandler;
}

declare module '@sapper/service-worker' {
Expand All @@ -95,7 +101,6 @@ declare module '@sapper/service-worker' {
}

declare module '@sapper/common' {

import type fetchType from 'node-fetch';
export type FetchResponse = Response | ReturnType<typeof fetchType>;

Expand All @@ -107,7 +112,7 @@ declare module '@sapper/common' {

export type PageParams = Record<string, string>;
export type Query = Record<string, string | string[]>;

export interface PageContext {
host: string;
path: string;
Expand Down
15 changes: 9 additions & 6 deletions runtime/src/internal/manifest-server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const build_dir: string;
export const dev: boolean;
export const manifest: Manifest;

export { SapperRequest, SapperResponse } from '@sapper/server';
export { SapperRequest, SapperResponse, SapperNext, SapperHandler, SapperErrorHandler } from '@sapper/server';

export interface SSRComponentModule {
default: SSRComponent;
Expand All @@ -25,9 +25,10 @@ export interface SSRComponent {
export interface Manifest {
server_routes: ServerRoute[];
ignore: RegExp[];
root_comp: SSRComponentModule
error: SSRComponent
pages: ManifestPage[]
root_comp: SSRComponentModule;
error: SSRComponent;
error_handler?: SapperErrorHandler;
pages: ManifestPage[];
}

export interface ManifestPage {
Expand All @@ -42,10 +43,12 @@ export interface ManifestPagePart {
params?: (match: RegExpMatchArray | null) => Record<string, string>;
}

export type Handler = (req: SapperRequest, res: SapperResponse, next: () => void) => void;
export interface HttpError extends Error {
statusCode?: number;
}

export interface ServerRoute {
pattern: RegExp;
handlers: Record<string, Handler>;
handlers: Record<string, SapperHandler>;
params: (match: RegExpMatchArray) => Record<string, string>;
}
107 changes: 107 additions & 0 deletions runtime/src/server/middleware/get_error_handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { sourcemap_stacktrace } from './sourcemap_stacktrace';
import {
Manifest,
SapperRequest,
SapperResponse,
SapperNext,
SapperErrorHandler,
dev
} from '@sapper/internal/manifest-server';
import { PageRenderer } from './get_page_handler';

export function get_error_handler(
manifest: Manifest,
page_renderer: PageRenderer
): SapperErrorHandler {
const { error_handler, error: error_route } = manifest;

function on_error(err) {
if (err instanceof Error && err.stack) {
err.stack = sourcemap_stacktrace(err.stack);
}

console.error(err);
}

function render_plain(err, req, res) {
if (!dev) {
if (res.statusCode === 404) {
return res.end('Not found');
} else {
return res.end('Internal server error');
}
}

let errText = err.toString();
if (err.stack) {
errText += `\n${err.stack}`;
}

const contentType = res.getHeader('Content-Type');
const sendsHtml = (
!contentType ||
contentType.toLowerCase().includes('text/html')
);
const needsHtml = (sendsHtml && res.headersSent);

if (needsHtml) {
errText = escape_html(errText);
} else {
res.setHeader('Content-Type', 'text/plain');
}

res.end(errText);
}

async function default_error_handler(err, req, res) {
on_error(err);

res.statusCode = err.status ?? err.statusCode ?? 500;

try {
await render_page(err, req, res);
} catch (renderErr) {
on_error(renderErr);
await render_plain(err, req, res);
}
}

function render_page(err, req, res) {
return page_renderer({
pattern: null,
parts: [
{ name: null, component: { default: error_route } }
]
}, req, res, err);
}

return async function handle_error(err: any, req: SapperRequest, res: SapperResponse, next: SapperNext) {
err = err || 'Unknown error';

if (error_handler) {
try {
await error_handler(err, req, res, (handler_err?: any) => {
process.nextTick(() => default_error_handler(handler_err || err, req, res));
});
} catch (handler_err) {
on_error(handler_err);

default_error_handler(err, req, res);
}
} else {
default_error_handler(err, req, res);
}
};
}

function escape_html(html: string) {
const chars: Record<string, string> = {
'"' : 'quot',
'\'': '#39',
'&': 'amp',
'<' : 'lt',
'>' : 'gt'
};

return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
}
Loading