Node.js HTTP 练习答案(二):19~33
19. 简单路由表
import * as http from "node:http";
type Handler = (req: http.IncomingMessage, res: http.ServerResponse) => void | Promise<void>;
const routes = new Map<string, Handler>([
["GET /users", (_req, res) => sendJson(res, [])],
["POST /users", (_req, res) => { res.statusCode = 201; sendJson(res, { created: true }); }],
]);
async function dispatch(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
const handler = routes.get((req.method ?? "GET") + " " + pathname);
if (handler) { await handler(req, res); return; }
const methods = [...routes.keys()].filter((key) => key.endsWith(" " + pathname)).map((key) => key.split(" ", 1)[0]!);
if (methods.length) { res.setHeader("Allow", methods.join(", ")); res.writeHead(405).end("Method Not Allowed"); return; }
res.writeHead(404).end("Not Found");
}
20. 统一错误响应
const server = http.createServer((req, res) => {
void dispatch(req, res).catch((error: unknown) => {
console.error(error);
if (!res.headersSent) {
res.statusCode = 500; sendJson(res, { error: "internal_error" });
} else {
res.destroy(error instanceof Error ? error : undefined);
}
});
});
Header 已发送后无法可靠改成 500 JSON,只能停止继续写并销毁不完整响应。
21. 简化中间件
type Middleware = (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void;
const middleware: Middleware[] = [
(req, _res, next) => { console.log(req.method, req.url); next(); },
(req, res, next) => {
if (req.headers["x-api-key"] !== "demo") { res.writeHead(401).end("Unauthorized"); return; }
next();
},
(_req, res) => res.end("ok"),
];
function run(index: number, req: http.IncomingMessage, res: http.ServerResponse): void {
const current = middleware[index];
if (current) current(req, res, () => run(index + 1, req, res));
}
next 把控制权交给后续层;真实 Express 还处理错误中间件、异步边界和路由匹配。
22. 请求日志
const startedAt = process.hrtime.bigint();
res.once("finish", () => {
const ms = Number(process.hrtime.bigint() - startedAt) / 1e6;
console.log(new Date().toISOString(), req.method, req.url, res.statusCode, ms.toFixed(1) + "ms");
});
finish 表示响应已交给底层,不保证客户端完整收到。还应监听 close 记录中断,并避免两个事件重复结算。
23. 客户端中断
if (req.url === "/slow") {
let disconnected = false;
req.once("aborted", () => { disconnected = true; });
res.once("close", () => { if (!res.writableFinished) disconnected = true; });
const timer = setTimeout(() => { if (!disconnected) res.end("done"); }, 5000);
res.once("close", () => clearTimeout(timer));
}
真实耗时任务还要把 AbortSignal 传给数据库、fetch 或子进程,仅不写响应不能停止后台工作。
24. 返回 HTML 文件
import { readFile } from "node:fs/promises";
try {
const html = await readFile("public/index.html");
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Length": html.length });
res.end(html);
} catch (error: unknown) {
if (isErrno(error) && error.code === "ENOENT") res.writeHead(404).end("Not Found");
else throw error;
}
25. 流式下载
import { createReadStream } from "node:fs";
import { pipeline } from "node:stream/promises";
res.setHeader("Content-Type", "application/octet-stream");
try { await pipeline(createReadStream("large.zip"), res); }
catch (error: unknown) {
if (!res.headersSent) {
if (isErrno(error) && error.code === "ENOENT") res.writeHead(404).end("Not Found");
else res.writeHead(500).end("Internal Server Error");
}
else res.destroy(error instanceof Error ? error : undefined);
}
pipeline 比单独 pipe 更适合等待完成和集中传播错误。
26. Content-Type 映射
const mime: Record<string, string> = {
".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8",
".png": "image/png", ".jpg": "image/jpeg", ".txt": "text/plain; charset=utf-8",
};
const type = mime[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
res.setHeader("Content-Type", type);
27. 防止路径穿越
import * as path from "node:path";
const root = path.resolve("public");
function resolveStatic(input: string): string {
const target = path.resolve(root, input.replaceAll("\\", "/"));
const relative = path.relative(root, target);
if (relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) throw new Error("Forbidden");
return target;
}
还要先安全 decode URL,并考虑符号链接/TOCTOU;高风险文件服务不要只靠字符串检查。
28. 缓存头
res.setHeader("Cache-Control", "public, max-age=60");
HTML 通常短缓存或 no-cache 以便更新入口;内容 Hash 的 JS/CSS 可使用 public, max-age=31536000, immutable。缓存策略必须与发布文件命名匹配。
29. http.get
http.get("http://127.0.0.1:8080/", (response) => {
console.log(response.statusCode, response.headers);
const chunks: Buffer[] = [];
response.on("data", (chunk: Buffer) => chunks.push(chunk));
response.on("end", () => console.log(Buffer.concat(chunks).toString("utf8")));
}).on("error", console.error);
30. http.request 发送 JSON
const body = JSON.stringify({ name: "Bob" });
const request = http.request({
hostname: "127.0.0.1", port: 8080, path: "/json", method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8", "Content-Length": Buffer.byteLength(body) },
}, (response) => response.pipe(process.stdout));
request.on("error", console.error);
request.write(body);
request.end();
31. 客户端网络错误
const request = http.get("http://127.0.0.1:9999/", (response) => response.resume());
request.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "ECONNREFUSED") console.error("目标端口拒绝连接");
else console.error(error);
});
response.resume 消费不需要的响应体,便于连接正确收尾。
32. 客户端超时
const request = http.get("http://127.0.0.1:8080/slow", (response) => response.resume());
request.setTimeout(2000, () => request.destroy(new Error("REQUEST_TIMEOUT")));
request.on("error", (error) => {
if (error.message === "REQUEST_TIMEOUT") console.error("请求超时");
else console.error("连接或传输失败", error);
});
setTimeout 只通知 socket 一段时间无活动,不会自动销毁请求,也不等于严格的总耗时 deadline。总期限更适合 AbortSignal.timeout 加明确取消。
33. 读取客户端响应流
const request = http.request("http://127.0.0.1:8080/download", async (response) => {
let totalBytes = 0;
try {
for await (const chunk of response) totalBytes += Buffer.from(chunk).length;
console.log({ status: response.statusCode, totalBytes });
} catch (error: unknown) { console.error("响应流失败", error); }
});
request.on("error", console.error);
request.end();
客户端 response 也是 IncomingMessage/Readable;Chunk 不是完整消息边界。
function isErrno(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && "code" in error;
}