理解 Node.js 的 HTTP 处理,可以从一对对象开始:request 负责接收客户端发来的信息,response 负责把信息写回客户端。前者是一条可读流,后者是一条可写流;方法、路径、头部、请求体和错误,都围绕这对对象展开。
本指南假定你已经大致了解 HTTP 请求,也接触过 Node.js 的 EventEmitter 与 Stream。若这些概念还不熟悉,可以先阅读对应 API 文档。下文按官方指南的顺序,从创建服务器一直讲到基于管道的 echo 服务。
原文:Node.js 文档贡献者的 Anatomy of an HTTP Transaction,2026年10月5日读取。原页提供 CJS 与 ESM 两种写法;本文合并解释,开头展示两种导入,后续完整示例统一使用 CJS。所有示例仅经静态审阅,未启动监听或发送请求。

创建服务器和请求处理函数
Node.js Web 服务首先需要一个服务器对象,通过 http.createServer 创建。CommonJS 写法如下:
const http = require('node:http');
const server = http.createServer((request, response) => {
// magic happens here!
});
若项目使用 ECMAScript 模块,改用对应导入语法;处理函数本身相同:
import http from 'node:http';
const server = http.createServer((request, response) => {
// magic happens here!
});
每当一个 HTTP 请求到达服务器,传给 createServer 的函数就会被调用一次,所以它称为请求处理函数。createServer 返回的 Server 本身也是 EventEmitter。把函数直接传进去,只是先创建对象、再监听 request 事件的简写:
const server = http.createServer();
server.on('request', (request, response) => {
// the same kind of magic happens here!
});
处理函数收到 request 和 response 两个对象,分别承载事务的输入和输出。要真正接受连接,还必须在服务器对象上调用 listen。端口是常用参数,其他选项见 HTTP API。
编辑补充:原文完整示例使用 .listen(8080),未指定监听主机,这可能让服务通过外部网络接口可达。本文保留原代码以便核对;在本地隔离实验中,结尾应明确改成 .listen(8080, '127.0.0.1')。这只限制监听范围,并不补齐 TLS、认证、限流或资源管理。
读取方法、URL 和请求头
处理请求时,通常先查看方法和 URL,决定执行什么操作。两者可直接从 request 读取:
const { method, url } = request;
request 是 IncomingMessage 的实例。method 表示 HTTP 方法,例如 GET 或 POST。在原文讨论的常见请求形式里,url 是去掉协议、主机和端口后的请求目标,通常包含路径及查询串。
const { headers } = request;
const userAgent = headers['user-agent'];
请求头位于 headers 对象中,其键名统一转成小写,不受客户端发送时大小写的影响。某些重复头可能被覆盖或合并为逗号分隔的字符串,具体取决于头字段规则。如果需要检查原始顺序和形式,可以查看 rawHeaders;不要假定所有重复字段都有完全相同的合并规则。
请求体是一条流
POST 或 PUT 请求中,请求体通常是应用关心的内容。它不像头部那样直接作为完整对象提供:request 实现了可读流接口,可以监听事件,也可以连接到其他流。最直接的读法是监听 data 和 end。
每次 data 事件给出一个 Buffer。如果已经确定最终应按字符串解释,可以先收集这些块,等流结束后再拼接并解码:
let body = [];
request
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// at this point, `body` has the entire request body stored in it as a string
});
先拼接 Buffer、再转换字符串,也避免了把一个跨块的多字节字符分别解码。toString() 的默认编码是 UTF-8,真实协议需要依据明确的内容类型和编码规则处理,二进制内容不应无条件转成文本。
这段逻辑比较琐碎,原文提到 npm 上的 concat-stream 和 body 可封装部分工作。理解底层发生了什么之后,再决定是否使用抽象更高的库。这些链接是原文提及的工具,不构成对当前维护状态或适用性的重新推荐。
编辑补充:数组会保留每个请求块,Buffer.concat 还会分配合并后的缓冲区,随后字符串又占用内存。原例没有请求体大小、读取时间或并发限制;攻击者或意外的大请求都可能耗尽资源。实际服务必须给这些资源设置边界,不能直接把这个教学写法用于公开接收任意数据。
别漏掉请求流的错误
可读流也是 EventEmitter。请求流发生错误时会发出 error 事件;如果没有监听器,未处理的错误可能让 Node.js 进程退出。因此,请求流需要显式错误处理,哪怕最初只是记录错误:
request.on('error', err => {
// This prints the error message and stack trace to `stderr`.
console.error(err.stack);
});
这里把错误信息与堆栈写到标准错误。实际应用还需要决定是否能向客户端发送合理的 HTTP 错误,以及怎样清理已占用资源。可以使用更高层的抽象,但不能因此假定错误不会发生。详情可查 Node.js 错误文档。
到这里,我们还没有发出响应
把创建服务器、读取方法、路径、头部和请求体组合起来,会得到下面这个中间阶段:
const http = require('node:http');
http
.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request
.on('error', err => {
console.error(err);
})
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// At this point, we have the headers, method, url and body, and can now
// do whatever we need to in order to respond to this request.
});
})
.listen(8080); // Activates this server, listening on port 8080.
这个例子故意只读取请求,没有向 response 写入数据,也没有结束响应。客户端会一直等,直到超时。不要把这一段截出来当作已经完成的服务器。
response 是 ServerResponse 的实例,也是一条可写流,提供状态码、响应头和响应体的写入方法。接下来逐一补上。
先设置状态码与响应头
没有另行设置时,响应状态码默认是200。若资源不存在,可以先改成404:
response.statusCode = 404; // Tell the client that the resource wasn't found.
响应头通过 setHeader 设置:
response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');
设置时,头字段名不区分大小写;对同一个头反复设置,最后的值会被发送。上面采用的是“隐式头部”:先保存状态和头部信息,让 Node.js 在开始写出响应体之前发送它们。
也可以使用 writeHead 明确提交状态码和头部:
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon',
});
关键顺序不变:先确定状态和头部,再开始发送响应体。响应头已经发送之后,不能再依靠修改 statusCode 或 setHeader 改变客户端已经收到的内容。
写响应体,并结束响应
由于响应是一条可写流,可以分块调用 write,最后调用 end:
response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();
end 也可以接收最后一段数据,因此上面的例子可简化为:
response.end('<html><body><h1>Hello, World!</h1></body></html>');
响应流同样可能发出 error 事件。对请求流的错误处理原则也适用于响应流:必须显式考虑并处理它,而不是只关注成功路径。
合在一起:以 JSON 回显请求
在原来的读取逻辑上加上响应,就能把收到的头部、方法、URL 和 body 组成一个对象,再用 JSON.stringify 发送给客户端:
const http = require('node:http');
http
.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request
.on('error', err => {
console.error(err);
})
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// BEGINNING OF NEW STUFF
response.on('error', err => {
console.error(err);
});
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
// Note: the 2 lines above could be replaced with this next one:
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body };
response.write(JSON.stringify(responseBody));
response.end();
// Note: the 2 lines above could be replaced with this next one:
// response.end(JSON.stringify(responseBody))
// END OF NEW STUFF
});
})
.listen(8080);
原文也指出,设置状态码与内容类型的两行,可以换成 writeHead(200, {'Content-Type': 'application/json'});最后的 write 加 end,可以换成 end(JSON.stringify(responseBody))。
编辑补充:这是观察 HTTP 数据流的教学工具。原样回传全部请求头和 body,可能暴露 Cookie、Authorization、口令或其他业务数据;不要把它部署成公开诊断端点,也不要把真实凭据发送给不受信任的 echo 服务。这里的错误监听仍以记录为主,不是完整的请求失败处理方案。
从聚合请求体改成流式 echo
先把上一段简化:服务器只把收到的请求体原样写回,不再构造 JSON:
const http = require('node:http');
http
.createServer((request, response) => {
let body = [];
request
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
})
.listen(8080);
接着限定路由:只有方法为 POST 且 URL 恰好为 /echo 才回显,其他情况返回404:
const http = require('node:http');
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
let body = [];
request
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
依据 URL 决定处理方式,就是一种路由。路由可以简单到一个 switch,也可以由 Express 等框架提供;原文还提到只负责路由的 router 包。
编辑补充:这里比较的是完整 request.url 字符串,所以 /echo?x=1 不等于 /echo,会进入404分支。实际路由需要按明确的 URL 解析规则区分 pathname 与查询参数,并处理非法请求目标;不要简单去掉任意子串来“修复”匹配。
回忆一下,request 可读,response 可写。既然 echo 的目标就是把输入传到输出,那么用 pipe 直接连接它们即可,无需先把完整请求聚合成一个字符串:
const http = require('node:http');
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
管道会按流机制传递数据,省去了这里的整包聚合。不过这还没有结束:错误依然可能发生。原文的最终例子在请求错误时记录日志并尝试返回400,在响应错误时记录日志:
const http = require('node:http');
http
.createServer((request, response) => {
request.on('error', err => {
console.error(err);
response.statusCode = 400;
response.end();
});
response.on('error', err => {
console.error(err);
});
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
在真实应用中,应该检查具体错误,再决定适合的状态码与消息,而不是把所有错误都解释成 Bad Request。尤其是流式输出已经开始之后,头部可能早已发出;若客户端断开,响应连接可能已经不可写,此时没有办法保证400还能送达。
编辑补充:错误处理要看响应所处阶段
下面是对原文请求错误监听器的局部修正示意:停止继续向响应管道传输,在尚未发送头、尚未结束且未销毁时才尝试写400,否则清理仍存活的响应。它用于说明状态检查,未经过运行验证,也不能替代完整的断连、超时、背压与资源回收设计。
request.on('error', err => {
console.error(err);
request.unpipe(response);
if (!response.headersSent && !response.writableEnded && !response.destroyed) {
response.statusCode = 400;
response.end();
} else if (!response.destroyed) {
response.destroy();
}
});
本段与原文的差异是增加了管道解绑和响应状态判断;原文最终完整例子仍保留在前面供核对。pipe 可以处理正常的数据传输节奏,但不代表它替你完成了所有错误传播、客户端取消、大小上限、超时和并发策略。日志同样需要控制敏感内容和体积。
现在可以沿着事务逐步推理
这篇指南覆盖了创建带请求处理函数的 HTTP 服务器、监听端口、读取请求头与方法路径、读取 body、做路由判断、设置状态与响应头、写入并结束响应、把请求流接到响应流,以及分别处理两端错误。遇到行为不确定的地方,应继续查阅 EventEmitter、Stream 和 HTTP API,并按目标 Node.js 版本验证。
原页未为这些片段指定固定 Node.js 版本;本文也未将其标为已经在当前 LTS 实测通过。把教学代码变成可公开提供服务的程序,还需围绕协议约束和资源限制完成设计。
来源、归属与翻译说明
原文:Anatomy of an HTTP Transaction;源页面编辑入口。归属 Node.js 文档贡献者,页面版权声明为“Copyright OpenJS Foundation and Node.js contributors. All rights reserved.”。当前页面未单独列出内容许可证,当前文档仓库首页也未展示许可证文件;不另行推断为 MIT 或 Creative Commons 许可。
中文翻译、模块语法合并、资源与错误边界提示、局部修正代码和示意图为本稿编辑工作,均不代表原作者对本稿或任何产品的背书。












暂无评论内容