# 网络服务 http res
30 秒速记
- 响应分两段:先头(状态码 + 响应头),后体;第一次
write/end时头就发出去了 - 头发出后再
setHeader/writeHead会抛ERR_HTTP_HEADERS_SENT,用res.headersSent判断 write()返回false= 缓冲满,等drain再写- 状态码、
Content-Type、缓存头、安全头要设对;错误堆栈别直接回给客户端 - 流式响应中途出错,头已经是
200改不了,只能res.destroy()中断并记日志
res 是服务端往回写响应的对象,最关键的规则是头先于体:第一次写响应体的瞬间,状态码和响应头就已经发出去了。 之后再想改状态码或加头,Node 会直接抛 ERR_HTTP_HEADERS_SENT。这也是流式响应的一个现实问题:已经吐了一半数据才出错,没法再改成 500 加一段 JSON 错误,只能把连接断掉让客户端感知失败,同时记好日志。另外错误处理别把 err.stack 原样回给客户端,内部路径和依赖版本全暴露了。
理解「网络服务 http res」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
res 的生命周期可以用一个例子看清楚:
const http = require('http')
http.createServer((req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
console.log(res.headersSent) // false,头还在内存里,可以随便改
res.write('第一段\n')
console.log(res.headersSent) // true,第一次 write 时头已经发出
try {
res.setHeader('X-Late', '1') // 头发出后再改
} catch (err) {
console.log(err.code) // ERR_HTTP_HEADERS_SENT
}
res.end('结束\n')
}).listen(3000)
几个规则:
res.statusCode和res.setHeader只是改内存里的值,真正发送发生在第一次write、end或者调用writeHead的时候。writeHead会立即把头写出,它传入的头会和之前setHeader设的合并,同名时以writeHead为准。- 只调
res.end(data)且没写过别的内容时,Node会自动算Content-Length;多次write又没设长度,就自动用Transfer-Encoding: chunked。 - 写入有背压:
res.write()返回false时等drain事件再继续,大文件直接pipeline交给流处理。
面试官追问
追问 1报错 Cannot set headers after they are sent to the client,一般是什么原因?
同一个请求被响应了两次,常见的是异步回调里 res.end() 后没 return,下面又走到另一个 res.end() 或 setHeader。在错误处理里先判断 if (res.headersSent) return。
追问 2流式导出写到一半数据库报错了,怎么让前端知道失败?
头已经是 200 发出去了改不了,只能 res.destroy(err) 掐断连接,前端会收到一个不完整的响应或网络错误。要求更严的话,在内容最后加一个结束标记,前端没读到标记就当失败。
追问 3错误处理里把 err.stack 返回给前端方便调试,可以吗?
生产环境不行,堆栈里有文件路径、依赖名和版本,等于给攻击者递地图。返回一个错误码和 requestId,详细堆栈只进日志,排查时按 requestId 去查。
追问 4不设 Content-Type 直接 res.end(JSON.stringify(data)) 有什么问题?
Node 不会帮你补 Content-Type,有的客户端会当纯文本处理,axios 也可能不自动解析。另外缺 X-Content-Type-Options: nosniff 时浏览器可能自己猜类型,有安全隐患。
# 概览
30 秒速记
res的职责:把状态码 / 状态描述、响应头、响应体返回给客户端- 最小写法
res.end('ok'):状态码默认200、描述默认OK Node会自动补Date、Connection、Content-Length这类头,但不会补Content-Type- 状态码要反映真实结果:失败分支还回
200 ok,监控和调用方都会被骗 - 想看真实响应用
curl -i,别只看浏览器页面上的文字
res 负责把一个完整的 HTTP 响应发回去,包括状态码、响应头和响应体三部分。 最简单的 res.end('ok') 也是一个完整响应:状态码默认 200,Node 自动带上 Date、Content-Length 和 Connection 头。但它不会替你判断内容类型,Content-Type 要自己设;状态码也只会是默认值,失败分支要自己改。我验证接口时习惯 curl -i localhost:3000,三部分一眼看全。
http模块四剑客之一的res,应该都不陌生了。一个web服务程序,接受到来自客户端的http请求后,向客户端返回正确的响应内容,这就是res的职责。
返回的内容包括:状态代码/状态描述信息、响应头部、响应主体。下文会举几个简单的例子。
var http = require('http');
var server = http.createServer(function(req, res){
res.end('ok');
});
server.listen(3000);
面试官追问
追问 1res.end('ok') 实际发出去的响应长什么样?
curl -i 能看到:HTTP/1.1 200 OK,加上 Date、Connection: keep-alive、Keep-Alive: timeout=5、Content-Length: 2 几个头,然后是 ok。没有 Content-Type。
追问 2健康检查接口所有分支都 res.end('ok'),有什么问题?
数据库挂了它也返回 200,负载均衡和监控只看状态码,会继续把流量往这个坏实例上打。依赖不可用时要回 503,让探测真正能摘掉实例。
追问 3监控报接口异常,但浏览器打开显示正常,先核对什么?
先对齐是不是同一个请求:监控可能带了不同的头、走了不同的入口或者有缓存。用 curl -i 模拟监控的请求,看状态码和响应头,而不是比页面上的文字。
追问 4简单返回一段文本,有必要引入 Express 吗?
没必要,http.createServer 加 res.end 就够了,少一个依赖少一个风险。路由、中间件、参数校验多起来了再换框架。
# 例子
30 秒速记
res.writeHead(200, 'ok', { 'Content-Type': 'text/plain' })一次写完状态码、描述和头- 再
res.end('hello')写响应体并结束 - 另一种写法:
res.statusCode = 200+res.setHeader(...),到第一次写体时才发 writeHead会立即发头,调用后不能再改;之前setHeader的头会被合并进来- 状态描述(
reason phrase)在HTTP/2里已经没有了,客户端别依赖它
这个例子用 res.writeHead 一次把状态码、状态描述和响应头发出去,再用 res.end 发响应体并结束。 要分清两种写法:writeHead 是马上把头写出去,之后改不了;statusCode 加 setHeader 是先存在内存里,等第一次写体时才发,中间还能改。我写中间件时倾向后者,因为后面的环节可能还要补头;最终处理函数里头已经确定了,用 writeHead 一行写完更紧凑。
在下面的例子中,我们同时设置了 状态代码/状态描述信息、响应头部、响应主体,就是这么简单。
var http = require('http');
// 设置状态码、状态描述信息、响应主体
var server = http.createServer(function(req, res){
res.writeHead(200, 'ok', {
'Content-Type': 'text/plain'
});
res.end('hello');
});
server.listen(3000);
面试官追问
追问 1writeHead 之后再调 setHeader 会怎样?
抛 ERR_HTTP_HEADERS_SENT,因为 writeHead 已经把头写出去了。反过来先
