Files
pikasTech-HWLAB/internal/sim/http.mjs
T
2026-05-25 03:29:52 +00:00

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}`);
});
}