Node.js HTTP 练习答案(一):1~18
对应题目:http.md。分册:19~33 · 34~42 · 43~50。
以下基于 Node.js 22+ 和 TypeScript。为便于学习,部分题复用后文辅助函数。
1. 请求基本信息
import * as http from "node:http";
const port = 8080;
const server = http.createServer((req, res) => {
console.log({
method: req.method,
url: req.url,
headers: req.headers,
remoteAddress: req.socket.remoteAddress,
remotePort: req.socket.remotePort,
});
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("ok");
});
server.listen(port);
remoteAddress 可能是代理地址;生产环境的真实客户端 IP 还涉及可信代理。
2. URL、pathname 和 query
const url = new URL(req.url ?? "/", "http://localhost");
console.log(url.pathname); // /users
console.log(url.searchParams.get("id"));
console.log(url.searchParams.get("active"));
服务器收到的是 request-target,通常只有路径和查询;base 只用于让 URL 构造器完成解析,不代表真实公网 Origin。
3. 判断方法
const url = new URL(req.url ?? "/", "http://localhost");
if (url.pathname !== "/users") {
res.writeHead(404).end("Not Found"); return;
}
if (req.method === "GET") {
sendJson(res, [{ id: 1 }]); return;
}
if (req.method === "POST") {
res.statusCode = 201; sendJson(res, { created: true }); return;
}
res.setHeader("Allow", "GET, POST");
res.writeHead(405).end("Method Not Allowed");
405 表示资源存在但方法不允许,最好同时返回 Allow。
4. 状态码
const statuses: Record<string, number> = {
"/ok": 200, "/created": 201, "/empty": 204,
"/bad-request": 400, "/not-found": 404, "/error": 500,
};
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
const status = statuses[pathname] ?? 404;
res.statusCode = status;
if (status === 204) res.end();
else res.end(http.STATUS_CODES[status] ?? "");
204 响应不能包含消息体。
5. Header 与 Body
const body = JSON.stringify({ message: "hello" });
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(body);
Content-Type 是浏览器/客户端解释 Body 的元数据,不会把普通字符串自动变成 JSON。
6. 纯文本
function sendText(res: http.ServerResponse, text: string): void {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(text);
}
sendText(res, "你好");
7. JSON
function sendJson(res: http.ServerResponse, data: unknown): void {
const body = JSON.stringify(data);
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.setHeader("Content-Length", Buffer.byteLength(body, "utf8"));
res.end(body);
}
JSON.stringify 也可能因 BigInt 或循环引用抛错,最好在发送 Header 前完成序列化。
8. HTML
function sendHtml(res: http.ServerResponse, html: string): void {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Content-Length", Buffer.byteLength(html, "utf8"));
res.end(html);
}
sendHtml(res, "<!doctype html><html><body>你好</body></html>");
9. 字符数与字节数
const text = "你好 HTTP";
console.log(text.length); // UTF-16 code unit 数
console.log(Buffer.byteLength(text, "utf8")); // UTF-8 字节数
Content-Length 定义的是字节数,不能使用字符串 length。
10. 避免重复响应
错误形式是在分支 res.end 后继续执行后面的 res.end。正确写法:
function handler(req: http.IncomingMessage, res: http.ServerResponse): void {
if (req.method !== "GET") {
res.writeHead(405).end("Method Not Allowed");
return;
}
res.end("ok");
}
重复修改已发送 Header 常见 ERR_HTTP_HEADERS_SENT;重复结束/写入还可能产生 Stream 错误。每个终止分支都 return。
11. setHeader 与 writeHead
res.statusCode = 200;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end("{}");
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end("{}");
setHeader 适合中间过程逐步构造和覆盖;writeHead 一次确定状态和 Header,并会立即发送 Header。writeHead 中同名值优先于之前 setHeader 的值。
12. 读取文本 Body
async function readBody(
req: http.IncomingMessage,
maxBytes = 1024 * 1024,
): Promise<Buffer> {
const chunks: Buffer[] = [];
let total = 0;
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
total += buffer.length;
if (total > maxBytes) {
const error = new Error("Payload Too Large");
Object.assign(error, { code: "BODY_TOO_LARGE" });
throw error;
}
chunks.push(buffer);
}
return Buffer.concat(chunks, total);
}
if (req.method === "POST" && req.url === "/echo") {
const body = await readBody(req);
sendText(res, body.toString("utf8"));
}
测试:curl -i -X POST http://localhost:8080/echo -d “hello”。
13. JSON Body
try {
const text = (await readBody(req)).toString("utf8");
const value = JSON.parse(text) as unknown;
sendJson(res, { received: value });
} catch (error: unknown) {
if (error instanceof SyntaxError) {
res.statusCode = 400; sendJson(res, { error: "invalid_json" });
} else {
throw error;
}
}
TypeScript 的 unknown 不会验证 JSON 业务结构,后续仍需类型守卫或 Schema 校验。
14. 空 Body
本答案选择返回 400:
const body = await readBody(req);
if (body.length === 0) {
res.statusCode = 400;
sendJson(res, { error: "body_required" });
return;
}
const value = JSON.parse(body.toString("utf8")) as unknown;
把空 Body 当作空对象也可以,但必须形成明确、稳定的 API 契约。
15. Body 大小限制
第 12 题的 readBody 在每个 Chunk 到达时累计 Buffer.length,超过上限立即抛出,而不是完整缓存后判断。调用处:
try {
await readBody(req, 1024 * 1024);
} catch (error: unknown) {
if (
error instanceof Error
&& "code" in error
&& error.code === "BODY_TOO_LARGE"
) {
res.writeHead(413, { Connection: "close" }).end("Payload Too Large");
req.destroy();
return;
}
throw error;
}
销毁连接会使响应能否完整送达取决于时机。生产实践也可以发送 413 后继续丢弃剩余 Body,以便安全复用连接;无论选择哪种都必须有界。
16. 中文 Chunk 边界
UTF-8 汉字可能被拆到两个 Chunk。分别对每块 toString 会把不完整序列转为替换字符;先保存 Buffer,再 concat 后统一解码能恢复完整字符。流式文本处理也可使用 StringDecoder:
import { StringDecoder } from "node:string_decoder";
const decoder = new StringDecoder("utf8");
let text = "";
for await (const chunk of req) text += decoder.write(Buffer.from(chunk));
text += decoder.end();
17. 按 Content-Type 解析
const rawType = req.headers["content-type"] ?? "";
const mediaType = rawType.split(";", 1)[0]!.trim().toLowerCase();
const body = await readBody(req);
if (mediaType === "application/json") {
sendJson(res, { received: JSON.parse(body.toString("utf8")) });
} else if (mediaType === "text/plain") {
sendJson(res, { received: body.toString("utf8") });
} else {
res.statusCode = 415;
sendJson(res, { error: "unsupported_media_type" });
}
真实项目还要单独捕获非法 JSON,并按 charset 契约处理。
18. Content-Length 预检查
const maximum = 1024 * 1024;
const header = req.headers["content-length"];
if (header !== undefined) {
const declared = Number(header);
if (!Number.isSafeInteger(declared) || declared < 0) {
res.writeHead(400).end("Invalid Content-Length"); return;
}
if (declared > maximum) {
res.writeHead(413, { Connection: "close" }).end(); req.destroy(); return;
}
}
const body = await readBody(req, maximum);
客户端可省略或谎报 Content-Length,chunked 请求也没有它,所以 Header 只能早拒绝,流式计数才是最终边界。Node HTTP 解析器会拒绝一部分协议层冲突,但业务仍需限制实际 Body。