Skip to content

Commit

Permalink
feat: support _404 and _500 templates
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed May 7, 2024
1 parent b7b9f75 commit ee93170
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 49 deletions.
75 changes: 32 additions & 43 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import { renderToString } from "preact-render-to-string";
import { FinishSetup, ForgotBuild } from "./finish_setup.tsx";
import { HttpError } from "./error.ts";

const DEFAULT_NOT_FOUND = () =>
Promise.resolve(new Response("Not found", { status: 404 }));
const DEFAULT_NOT_ALLOWED_METHOD = () =>
Promise.resolve(new Response("Method not allowed", { status: 405 }));
const DEFAULT_NOT_FOUND = () => {
throw new HttpError(404);
};
const DEFAULT_NOT_ALLOWED_METHOD = () => {
throw new HttpError(405);
};

export type ListenOptions = Partial<Deno.ServeTlsOptions> & {
remoteAddress?: string;
Expand Down Expand Up @@ -163,39 +165,6 @@ export class App<State> {
return this;
}

async #process(
req: Request,
params: Record<string, string>,
handlers: MiddlewareFn<State>[][],
next: () => Promise<Response>,
) {
const ctx = new FreshReqContext<State>(
req,
this.config,
next,
this.#islandRegistry,
this.#buildCache!,
);

ctx.params = params;

try {
if (handlers.length === 1 && handlers[0].length === 1) {
return handlers[0][0](ctx);
}

ctx.next = next;
return await runMiddlewares(handlers, ctx);
} catch (err) {
if (err instanceof HttpError) {
return new Response(null, { status: err.status });
}

console.error(err);
return new Response("Internal server error", { status: 500 });
}
}

async handler(): Promise<
(request: Request, info?: Deno.ServeHandlerInfo) => Promise<Response>
> {
Expand All @@ -207,24 +176,44 @@ export class App<State> {
return missingBuildHandler;
}

return (req: Request) => {
return async (req: Request) => {
const url = new URL(req.url);
// Prevent open redirect attacks
url.pathname = url.pathname.replace(/\/+/g, "/");

const method = req.method.toUpperCase() as Method;
const matched = this.#router.match(method, url);

const fallback = matched.patternMatch && !matched.methodMatch
const next = matched.patternMatch && !matched.methodMatch
? DEFAULT_NOT_ALLOWED_METHOD
: DEFAULT_NOT_FOUND;

return this.#process(
const { params, handlers } = matched;
const ctx = new FreshReqContext<State>(
req,
matched.params,
matched.handlers,
fallback,
this.config,
next,
this.#islandRegistry,
this.#buildCache!,
);

ctx.params = params;

try {
if (handlers.length === 1 && handlers[0].length === 1) {
return handlers[0][0](ctx);
}

ctx.next = next;
return await runMiddlewares(handlers, ctx);
} catch (err) {
if (err instanceof HttpError) {
return new Response(err.message, { status: err.status });
}

console.error(err);
return new Response("Internal server error", { status: 500 });
}
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/app_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ Deno.test("FreshApp - wrong method match", async () => {

let res = await server.put("/");
expect(res.status).toEqual(405);
expect(await res.text()).toEqual("Method not allowed");
expect(await res.text()).toEqual("Method Not Allowed");

res = await server.post("/");
expect(res.status).toEqual(200);
Expand Down
136 changes: 136 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,149 @@
import { MODE } from "./runtime/server/mod.tsx";

export function getMessage(status: number): string {
switch (status) {
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 102:
return "Processing (WebDAV)";
case 103:
return "Early Hints";
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 207:
return "Multi-Status (WebDAV)";
case 208:
return "Already Reported (WebDAV)";
case 226:
return "IM Used (HTTP Delta encoding)";
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy Deprecated";
case 306:
return "unused";
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required Experimental";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Payload Too Large";
case 414:
return "URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 418:
return "I'm a teapot";
case 421:
return "Misdirected Request";
case 422:
return "Unprocessable Content (WebDAV)";
case 423:
return "Locked (WebDAV)";
case 424:
return "Failed Dependency (WebDAV)";
case 425:
return "Too Early Experimental";
case 426:
return "Upgrade Required";
case 428:
return "Precondition Required";
case 429:
return "Too Many Requests";
case 431:
return "Request Header Fields Too Large";
case 451:
return "Unavailable For Legal Reasons";
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
case 506:
return "Variant Also Negotiates";
case 507:
return "Insufficient Storage (WebDAV)";
case 508:
return "Loop Detected (WebDAV)";
case 510:
return "Not Extended";
case 511:
return "Network Authentication Required";
default:
return "Internal Server Error";
}
}

export class HttpError {
#error: Error | null = null;
name = "HttpError";
message: string;

constructor(
public status: number,
message: string = getMessage(status),
public options?: ErrorOptions,
) {
this.message = message;
if (MODE !== "production") {
this.#error = new Error();
}
Expand Down
42 changes: 41 additions & 1 deletion src/plugins/fs_routes/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { type Method, pathToPattern } from "../../router.ts";
import { type HandlerFn, isHandlerByMethod } from "../../handlers.ts";
import { type FsAdapter, fsAdapter } from "../../fs.ts";
import type { PageProps } from "../../runtime/server/mod.tsx";
import { HttpError } from "../../error.ts";

const TEST_FILE_PATTERN = /[._]test\.(?:[tj]sx?|[mc][tj]s)$/;
const GROUP_REG = /(^|[/\\\\])\((_[^/\\\\]+)\)[/\\\\]/;
Expand Down Expand Up @@ -183,6 +184,12 @@ export async function fsRoutes<State>(
} else if (normalized.endsWith("/_error")) {
stack.push(routeMod);
continue;
} else if (normalized.endsWith("/_404")) {
stack.push(routeMod);
continue;
} else if (normalized.endsWith("/_500")) {
stack.push(routeMod);
continue;
}

// Remove any elements not matching our parent path anymore
Expand Down Expand Up @@ -227,7 +234,7 @@ export async function fsRoutes<State>(
}
}

if (mod.path.endsWith("/_error")) {
if (mod.path.endsWith("/_error") || mod.path.endsWith("/_500")) {
const handlers = mod.handlers;
const handler = handlers === null ||
(isHandlerByMethod(handlers) && Object.keys(handlers).length === 0)
Expand All @@ -243,6 +250,22 @@ export async function fsRoutes<State>(
continue;
}

if (mod.path.endsWith("/_404")) {
const handlers = mod.handlers;
const handler = handlers === null ||
(isHandlerByMethod(handlers) && Object.keys(handlers).length === 0)
? undefined
: typeof handlers === "function"
? handlers
: undefined; // FIXME: Method handler
const notFoundComponents = components.slice();
if (mod.component !== null) {
notFoundComponents.push(mod.component);
}
app.use(notFoundMiddleware(notFoundComponents, handler));
continue;
}

if (mod.component !== null) {
components.push(mod.component);
}
Expand Down Expand Up @@ -298,6 +321,23 @@ function errorMiddleware<State>(
};
}

function notFoundMiddleware<State>(
components: AnyComponent<PageProps<unknown, State>>[],
handler: HandlerFn<unknown, State> | undefined,
): MiddlewareFn<State> {
const mid = renderMiddleware<State>(components, handler);
return async function notFoundMiddleware(ctx) {
try {
return await ctx.next();
} catch (err) {
if (err instanceof HttpError && err.status === 404) {
return mid(ctx);
}
throw err;
}
};
}

async function walkDir(
dir: string,
callback: (entry: WalkEntry) => void,
Expand Down
Loading

0 comments on commit ee93170

Please sign in to comment.