•
阅读时长 2 分钟

流式输出经 Nginx 二次验证问题

目录

Node 接口已经逐段返回,浏览器却等到最后才看到全部内容,问题可能出在反向代理。应用层的流式响应,需要经过代理以后仍然按过程交付。

Nginx 同时负责 HTTPS 和转发时,可以分两层检查:直连 Node 有没有逐段输出,再看代理后的行为。

一个每半秒发一条的接口

创建空目录,保存 server.mjs:

import http from "node:http";
http
  .createServer((req, res) => {
    if (req.url !== "/stream") {
      res.writeHead(404);
      return res.end();
    }
    res.writeHead(200, {
      "Content-Type": "application/x-ndjson; charset=utf-8",
      "Cache-Control": "no-store",
    });
    res.flushHeaders();
    let index = 0;
    const timer = setInterval(() => {
      res.write(JSON.stringify({ index, time: Date.now() }) + "\n");
      if (++index === 3) {
        clearInterval(timer);
        res.end();
      }
    }, 500);
    res.on("close", () => clearInterval(timer));
  })
  .listen(3000, "0.0.0.0");

node server.mjs 运行后,在另一个终端执行 curl -N http://127.0.0.1:3000/stream。-N 关闭 curl 自身的输出缓冲,避免客户端工具把现象藏起来。应看到三条 JSON 逐步出现。

HTTP 数据块和 JSON 行不是一一对应的关系。这个命令适合人工观察,真正消费数据时仍要按换行解析。

配置代理和本地 HTTPS

创建 Dockerfile:

FROM node:22.14.0-bookworm-slim
WORKDIR /app
COPY --chown=node:node server.mjs ./
USER node
EXPOSE 3000
CMD ["node", "server.mjs"]

创建 nginx.conf:

server {
  listen 443 ssl;
  server_name localhost;
  ssl_certificate /etc/nginx/certs/cert.pem;
  ssl_certificate_key /etc/nginx/certs/key.pem;
  location / {
    proxy_pass http://app:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_buffering off;
    proxy_read_timeout 30s;
    gzip off;
  }
}

proxy_pass 的 app 是下面 Compose 的服务名。Host 和 X-Forwarded-Proto 把原始请求信息传给应用;应用是否信任这些头,仍然要按实际代理拓扑配置。

proxy_buffering off 关闭这个 location 的代理响应缓冲。gzip 在这个小例子里关闭,便于减少观察变量,不代表所有生产流式接口都必须关闭压缩。

proxy_read_timeout 约束两次上游读取之间的等待,不是整个响应必须在 30 秒内结束。长时间不发送任何内容的接口,可能需要心跳和合适的超时。

创建 certs 目录。在有 OpenSSL 的终端生成仅用于本地的自签证书:

openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout certs/key.pem -out certs/cert.pem -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

私钥不放进 Git。创建 compose.yaml:

services:
  app:
    build: .
    ports:
      - "127.0.0.1:4312:3000"
  proxy:
    image: nginx:1.26.3-bookworm
    depends_on:
      - app
    ports:
      - "127.0.0.1:4313:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro

直连和代理分开观察

docker compose config --quiet
docker compose up --build -d
docker compose exec proxy nginx -t
curl -N http://127.0.0.1:4312/stream
curl -k -N https://localhost:4313/stream
docker compose logs app proxy

-k 只是本地自签证书演示跳过证书校验,正式访问不应照搬。先确认 nginx -t 通过,再比较两条 curl 命令是否都是逐条输出。

目前只完成直连 Node 的流式检查和 Compose 配置检查;本机 Docker 后端不可用,Nginx 容器、TLS 和代理后分段到达的行为还需要在可运行的环境验证。

代理缓冲的官方说明见 Nginx proxy_buffering。如果 Nginx 后面还有 CDN、网关,它们也可能影响响应。关闭一层缓冲不能替其他层作保证。