-
Notifications
You must be signed in to change notification settings - Fork 0
/
handler.ts
132 lines (124 loc) · 3.63 KB
/
handler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { STATUS_CODE } from "@std/http/status";
import { ZodError, type ZodType } from "zod";
import { type Awaitable, settled } from "./async.ts";
import { fail } from "./fail.ts";
import { HttpError } from "./http_error.ts";
export * from "@std/http/status";
export * from "./http_error.ts";
type Merge<T, U> = Omit<T, keyof U> & U;
export type Handler<C = unknown> = (
req: Request,
ctx: C,
) => Awaitable<Response>;
export function toFetch(handler: Handler): (req: Request) => Promise<Response> {
return async (req) => {
const headersOnly = req.method === "HEAD";
if (headersOnly) {
req = new Request(req, { method: "GET" });
}
let res = await handler(req, null);
if (headersOnly) {
res = new Response(null, res);
}
return res;
};
}
export function logTime<C>(handler: Handler<C>): Handler<C> {
return async (req, ctx) => {
const start = performance.now();
const result = await settled(handler(req, ctx));
const end = performance.now();
const rt = (end - start).toFixed(1);
if (result.status === "rejected") {
const error = result.reason;
console.warn(`${req.method} ${req.url} - ${rt} ms - ${error}`);
throw error;
}
const res = result.value;
res.headers.append("server-timing", `rt;dur=${rt}`);
console.log(`${req.method} ${req.url} - ${rt} ms - ${res.status}`);
return res;
};
}
export function reportHttpErrors<C>(handler: Handler<C>): Handler<C> {
return async (req, ctx) => {
try {
return await handler(req, ctx);
} catch (e) {
if (e instanceof HttpError) {
const { message, status, headers } = e;
return Response.json({ error: message }, { status, headers });
}
throw e;
}
};
}
export function route<C>(
routes: {
readonly [pathname: string]: Handler<
Merge<C, { readonly params: Record<string, string | undefined> }>
>;
},
fallback: Handler<C>,
): Handler<C> {
const entries = Object.entries(routes).map(([pathname, handler]) => ({
pattern: new URLPattern({ pathname }),
handler,
}));
return async (req, ctx) => {
const url = new URL(req.url);
const path = (ctx as { params?: { 0?: string } } | null)?.params?.[0];
if (path !== undefined) {
url.pathname = path;
}
for (const { pattern, handler } of entries) {
const match = pattern.exec(url);
if (match) {
return await handler(req, { ...ctx, params: match.pathname.groups });
}
}
return await fallback(req, ctx);
};
}
export function methods<C>(methods: Record<string, Handler<C>>): Handler<C> {
const methodMap = new Map<string, Handler<C>>();
for (const method in methods) {
methodMap.set(method, methods[method]);
}
return (req, ctx) => {
const handler = methodMap.get(req.method) ?? fail(
new HttpError(
`Method ${req.method} is not allowed for the URL`,
"MethodNotAllowed",
{
headers: [
["allow", Array.from(methodMap.keys()).join(", ")],
],
},
),
);
return handler(req, ctx);
};
}
export function parseBodyAsJson<T, C>(
T: ZodType<T>,
handler: Handler<Merge<C, { readonly body: T }>>,
): Handler<C> {
return async (req, ctx) => {
let body: T;
try {
body = T.parse(await req.json());
} catch (e) {
if (e instanceof SyntaxError) {
throw new HttpError(e.message, "BadRequest");
}
if (e instanceof ZodError) {
return Response.json({ error: "Cannot parse body", issues: e.issues }, {
status: STATUS_CODE.BadRequest,
});
}
throw e;
}
return await handler(req, { ...ctx, body });
};
}