-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathserve.ts
More file actions
164 lines (151 loc) · 5.3 KB
/
serve.ts
File metadata and controls
164 lines (151 loc) · 5.3 KB
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* This module contains a basic version of a function like `Deno.serve`,
* implemented using `node:http`.
* @module
*/
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { Http2ServerResponse } from 'node:http2';
// deno-lint-ignore-file no-explicit-any
/**
* Basic version of a function like `Deno.serve`,
* implemented using `node:http`. To start a server:
*
* ```
* import { serve } from "@mastrojs/mastro/node";
* import mastro from "@mastrojs/mastro/server";
* serve(mastro.fetch);
* ```
*
* lightly adapted from:
* https://github.com/withastro/astro/blob/db8f8becc9508fa4f292d45c14af92ba59c414d1/packages/astro/src/core/app/node.ts#L55
* (MIT License)
*/
export const serve = (
handler: (r: Request) => Response | Promise<Response>,
opts?: { port?: number; },
): void => {
const { port = 8000 } = opts || {};
const server = createServer(async (req, res) => {
const standardReq = createRequest(req);
const standardRes = await handler(standardReq);
await writeResponse(standardRes, res);
});
server.on('error', e => {
console.error(e);
});
server.listen(port, () => console.log(`Listening on http://localhost:${port}`));
}
/**
* Streams a web-standard Response into a NodeJS Server Response.
*/
const writeResponse = async (standardRes: Response, res: ServerResponse): Promise<void> => {
const { status, headers, body, statusText } = standardRes;
// HTTP/2 doesn't support statusMessage
if (!(res instanceof Http2ServerResponse)) {
res.statusMessage = statusText;
}
res.writeHead(status, Object.fromEntries(headers.entries()));
if (!body) {
res.end();
return;
}
try {
const reader = body.getReader();
res.on('close', () => {
// Cancelling the reader may reject not just because of
// an error in the ReadableStream's cancel callback, but
// also because of an error anywhere in the stream.
reader.cancel().catch((err) => {
console.error(
`There was an uncaught error in the middle of the stream while rendering ${
res.req.url}.`,
err,
);
});
});
let result = await reader.read();
while (!result.done) {
res.write(result.value);
result = await reader.read();
}
res.end();
// the error will be logged by the "on end" callback above
} catch (err) {
res.write('Internal server error', () => {
err instanceof Error ? res.destroy(err) : res.destroy();
});
}
}
/**
* Converts a NodeJS IncomingMessage into a web standard Request.
*/
const createRequest = (req: IncomingMessage): Request => {
const method = req.method || 'GET';
const options: RequestInit = {
method,
headers: makeRequestHeaders(req),
...(method === 'HEAD' || method === 'GET' ? {} : asyncIterableToBodyProps(req)),
};
return new Request(getUrl(req), options);
}
const getUrl = (req: IncomingMessage): URL => {
// Get the used protocol between the end client and first proxy.
// NOTE: Some proxies append values with spaces and some do not.
// We need to handle it here and parse the header correctly.
// @example "https, http,http" => "http"
const forwardedProtocol = getFirstValue(req.headers['x-forwarded-proto']);
const providedProtocol = ('encrypted' in req.socket && req.socket.encrypted)
? 'https'
: 'http';
const protocol = forwardedProtocol ?? providedProtocol;
// @example "example.com,www2.example.com" => "example.com"
const forwardedHostname = getFirstValue(req.headers['x-forwarded-host']);
const providedHostname = req.headers.host ?? req.headers[':authority'];
const hostname = forwardedHostname ?? providedHostname;
// @example "443,8080,80" => "443"
const port = getFirstValue(req.headers['x-forwarded-port']);
try {
const hostnamePort = getHostnamePort(hostname, port);
return new URL(`${protocol}://${hostnamePort}${req.url}`);
} catch {
// Fallback to the provided hostname and port
const hostnamePort = getHostnamePort(providedHostname, port);
return new URL(`${providedProtocol}://${hostnamePort}`);
}
}
// Parses multiple header and returns first value if available.
const getFirstValue = (multiValueHeader?: string | string[]) =>
multiValueHeader?.toString()?.split(',').map((e) => e.trim())?.[0];
const getHostnamePort = (
hostname: string | string[] | undefined,
port?: string,
): string => {
const portInHostname = typeof hostname === 'string' && /:\d+$/.test(hostname);
return portInHostname ? hostname : `${hostname}${port ? `:${port}` : ''}`;
}
const makeRequestHeaders = (req: IncomingMessage): Headers => {
const headers = new Headers();
for (const [name, value] of Object.entries(req.headers)) {
if (value === undefined) {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
headers.append(name, item);
}
} else {
headers.append(name, value);
}
}
return headers;
}
const asyncIterableToBodyProps = (iterable: AsyncIterable<any>): RequestInit => {
return {
// @ts-expect-error Undici accepts a non-standard async iterable for the body.
body: iterable,
// The duplex property is required when using a ReadableStream or async
// iterable for the body. The type definitions do not include the duplex
// property because they are not up-to-date.
duplex: 'half',
};
}