138 lines
3.7 KiB
JavaScript
138 lines
3.7 KiB
JavaScript
import { createServer, request as httpRequest } from "node:http";
|
|
import { request as httpsRequest } from "node:https";
|
|
|
|
export class HttpError extends Error {
|
|
constructor(statusCode, body) {
|
|
super(body?.message ?? body?.error ?? `HTTP ${statusCode}`);
|
|
this.statusCode = statusCode;
|
|
this.body = body;
|
|
}
|
|
}
|
|
|
|
export function jsonResponse(res, statusCode, body) {
|
|
const payload = JSON.stringify(body, null, 2);
|
|
res.writeHead(statusCode, {
|
|
"content-type": "application/json; charset=utf-8",
|
|
"content-length": Buffer.byteLength(payload)
|
|
});
|
|
res.end(payload);
|
|
}
|
|
|
|
export async function readJson(req) {
|
|
const chunks = [];
|
|
for await (const chunk of req) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
if (!raw) {
|
|
return {};
|
|
}
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
export function createJsonServer({ routes }) {
|
|
return createServer(async (req, res) => {
|
|
try {
|
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
const key = `${req.method ?? "GET"} ${url.pathname}`;
|
|
const handler = routes.get(key);
|
|
|
|
if (!handler) {
|
|
jsonResponse(res, 404, {
|
|
error: "not_found",
|
|
path: url.pathname
|
|
});
|
|
return;
|
|
}
|
|
|
|
const body = await handler({ req, url });
|
|
jsonResponse(res, 200, body);
|
|
} catch (error) {
|
|
if (error instanceof HttpError) {
|
|
jsonResponse(res, error.statusCode, error.body);
|
|
return;
|
|
}
|
|
|
|
if (Number.isInteger(error?.statusCode) && error?.body) {
|
|
jsonResponse(res, error.statusCode, error.body);
|
|
return;
|
|
}
|
|
|
|
if (error instanceof SyntaxError) {
|
|
jsonResponse(res, 400, {
|
|
error: "invalid_json",
|
|
message: error.message
|
|
});
|
|
return;
|
|
}
|
|
|
|
jsonResponse(res, 500, {
|
|
error: "internal_error",
|
|
message: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function fetchJson(url, options = {}) {
|
|
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
const target = url instanceof URL ? url : new URL(String(url));
|
|
const client = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
const headers = {
|
|
...(body === undefined
|
|
? {}
|
|
: {
|
|
"content-type": "application/json",
|
|
"content-length": Buffer.byteLength(body)
|
|
}),
|
|
...(options.headers ?? {})
|
|
};
|
|
return new Promise((resolve, reject) => {
|
|
const request = client(target, {
|
|
method: options.method ?? (body === undefined ? "GET" : "POST"),
|
|
headers,
|
|
signal: options.signal
|
|
}, (response) => {
|
|
let text = "";
|
|
response.setEncoding("utf8");
|
|
response.on("data", (chunk) => {
|
|
text += chunk;
|
|
});
|
|
response.on("end", () => {
|
|
try {
|
|
const json = text ? JSON.parse(text) : null;
|
|
const status = response.statusCode ?? 0;
|
|
resolve({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
statusText: response.statusMessage ?? "",
|
|
body: json
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
request.on("error", reject);
|
|
if (body !== undefined) request.write(body);
|
|
request.end();
|
|
});
|
|
}
|
|
|
|
export function parsePort(value, fallback) {
|
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) {
|
|
return parsed;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export function listen(server, port, host = "0.0.0.0") {
|
|
server.listen(port, host, () => {
|
|
const address = server.address();
|
|
const bound = typeof address === "object" && address ? `${address.address}:${address.port}` : String(address);
|
|
console.log(`listening on ${bound}`);
|
|
});
|
|
}
|