侧边栏壁纸
博主头像
鱼箴日记,AI,Java,Liunx,Spring,Spring AI博主等级

行动起来,活在当下

  • 累计撰写 19 篇文章
  • 累计创建 12 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

OpenResty HTTPS 出现 `ERR_HTTP2_SERVER_REFUSED_STREAM`:一次由 `keepalive_timeout 0` 引发的排查

Administrator
2026-09-18 / 0 评论 / 0 点赞 / 1 阅读 / 18566 字 / 正在检测是否收录...

在一个 Vue/Vite 前端项目的测试环境中,HTTP 访问完全正常,但切换到 HTTPS 后,浏览器会批量出现下面的错误:

GET https://app.example.com:8443/assets/chunk-a1b2c3.js
net::ERR_HTTP2_SERVER_REFUSED_STREAM

GET https://app.example.com:8443/assets/styles-d4e5f6.css
net::ERR_HTTP2_SERVER_REFUSED_STREAM

Uncaught (in promise) Error: Unable to preload CSS

出现问题的环境使用 OpenResty 1.27.1.2-5-1-focal,HTTPS 由 OpenResty 直接提供,并启用了 HTTP/2:

server {
    listen 8443 ssl;
    http2 on;

    ssl_certificate     /etc/openresty/tls/example.crt;
    ssl_certificate_key /etc/openresty/tls/example.key;

    # 其他配置省略
}

最终定位到的关键配置是:

keepalive_timeout 0;
keepalive_requests 5000;

keepalive_timeout 改为非零值后,HTTPS 下的 HTTP/2 资源恢复正常:

keepalive_timeout 75s;
keepalive_requests 1000;

本文记录完整的判断过程,以及这个配置为什么会让问题只在 HTTPS + HTTP/2 下暴露出来。

一、问题现象

站点同时监听两个端口:

listen 8080;
listen 8443 ssl;
http2 on;

实际表现如下:

访问方式

协议

结果

http://app.example.com:8080

HTTP/1.1

正常

https://app.example.com:8443

HTTP/2(h2

大量 JS、CSS 加载失败

HTTPS 关闭 HTTP/2

HTTP/1.1 over TLS

正常

失败的不是单个文件,而是同一页面加载的多个静态资源,包括动态导入的 JavaScript、组件 CSS 和预加载资源。浏览器控制台还出现了:

Unable to preload CSS

这条错误不是根因。它只是说明 CSS 的网络请求已经失败,前端运行时无法完成 preload。

二、HTTP/2 支持 HTTPS,也与 Vue 无直接冲突

HTTP/2 和 HTTPS 属于不同层次:

  • HTTPS 表示 HTTP 通过 TLS 加密传输;

  • HTTP/1.1、HTTP/2 表示 HTTP 的协议版本;

  • 浏览器访问 HTTPS 时,会通过 TLS ALPN 协商使用 h2http/1.1

OpenResty/Nginx 支持在 HTTPS 上使用 HTTP/2。下面的配置是正常组合:

listen 8443 ssl;
http2 on;

Vue、React、Vite 等前端框架不会让 HTTP/2 失效。不过,现代前端项目通常会把页面拆分成大量 JS/CSS chunk,并通过动态导入和 preload 集中加载资源。这种请求模式会更容易暴露 HTTP/2 连接管理问题。

因此,前端资源数量是触发条件之一,但不是协议错误的根因。

三、ERR_HTTP2_SERVER_REFUSED_STREAM 表示什么

HTTP/2 会在一条 TCP/TLS 连接中承载多个独立的 stream。服务端可以通过 RST_STREAM 拒绝某个尚未处理的流,也可以通过 GOAWAY 表示连接不再接受后续流。

Chrome 收到相应信号后,可能显示:

ERR_HTTP2_SERVER_REFUSED_STREAM

它通常说明请求尚未进入正常的应用处理流程。它不等同于:

  • 静态文件不存在;文件不存在通常是 HTTP 404

  • TLS 证书错误;证书错误通常会发生在连接建立阶段;

  • Vue 组件代码错误;这里失败的是网络协议层请求;

  • gzip 不支持 HTTP/2。

四、关键根因:keepalive_timeout 0

问题环境的全局配置包含:

keepalive_timeout 0;
keepalive_requests 5000;

Nginx 官方文档对 keepalive_timeout 的定义很明确:第一个参数决定客户端持久连接在服务端保持的时间,值为 0 时禁用持久连接。默认值是 75s

这两项配置的意图实际上相互抵消:

  • keepalive_timeout 0 禁止连接复用;

  • keepalive_requests 5000 允许一条持久连接最多处理 5000 个请求;

  • 由于持久连接已被禁用,后者基本没有机会发挥作用。

HTTP/1.1 下,这通常表现为频繁重新建立连接,浏览器仍可能完成资源加载。HTTP/2 则依赖一条连接承载多个并发流:

一条 HTTPS/TCP 连接
├── stream 1:index.html
├── stream 3:index.js
├── stream 5:chunk-a.js
├── stream 7:styles-a.css
└── stream 9:vendor.js

页面先获取 HTML 和入口脚本,入口脚本执行后又会动态加载更多资源。如果服务端不允许连接保持和复用,连接可能在前一批请求结束后关闭或进入 GOAWAY 状态;浏览器随后仍尝试在原 HTTP/2 会话中创建资源流,部分流便可能被拒绝。

这正好解释了几个现象:

  1. HTTP 端口正常,HTTPS + HTTP/2 异常;

  2. 首页或少量资源能够加载,后续 JS/CSS 批量失败;

  3. keepalive_timeout 改为 75s 后恢复;

  4. Nginx 错误日志里不一定能找到失败资源的 URL。

Nginx 历史上也有一条专门讨论 keepalive_timeout 0 与 HTTP/2 的问题记录。早期“首个请求前连接就被关闭”的实现问题后来已修复,但 0 仍然明确表示禁用客户端持久连接,不适合常规浏览器 HTTP/2 页面。

五、修复配置

对于普通网站、管理后台和前端 SPA,可以先使用 Nginx 默认值:

http {
    keepalive_timeout 75s;
    keepalive_requests 1000;

    # 其他配置
}

如果希望只对 HTTPS 站点调整,也可以写在对应的 server 中:

server {
    listen 8443 ssl;
    http2 on;

    server_name app.example.com;

    ssl_certificate     /etc/openresty/tls/example.crt;
    ssl_certificate_key /etc/openresty/tls/example.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    keepalive_timeout 75s;
    keepalive_requests 1000;

    root /srv/www/example-app;
    index index.html;
    try_files $uri $uri/ /index.html;
}

对于该站点,60s 75s 都是合理起点。连接数非常多的公网服务可以根据内存、连接数和访问模型调整到 30s 60s,但启用 HTTP/2 时不建议直接设置为 0

应用配置前先检查语法:

/usr/local/openresty/nginx/sbin/nginx -t

检查通过后再重载:

/usr/local/openresty/nginx/sbin/nginx -s reload

六、为什么增加 sendfile 和 gzip 后看起来也修好了

排查过程中曾同时加入以下配置:

sendfile on;
keepalive_timeout 75s;

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/javascript text/javascript text/css application/xml;

修改后页面恢复,很容易误以为是 sendfile 或 gzip 修复了 HTTP/2。实际上:

  • sendfile on 可以提高静态文件发送效率;

  • gzip 可以减少 JS/CSS 的传输量,缩短流的存活时间;

  • gzip_http_version 1.1 表示允许压缩的最低 HTTP 版本,不会把 HTTP/2 降级为 HTTP/1.1;

  • 修改配置并 reload 也会创建新 worker,并促使客户端建立新连接。

这些因素可能降低问题出现的概率,但对比最终完整配置后,真正与故障直接对应的变化是:

- keepalive_timeout 0;
+ keepalive_timeout 75s;

排查此类问题时应尽量一次只修改一个变量,否则容易把性能优化配置误认为协议问题的根因。

七、为什么 error.log 中没有失败链接

这是 HTTP/2 问题中很容易造成误判的一点。

普通访问日志记录的是已经进入 HTTP 请求处理流程的请求。如果一个 HTTP/2 stream 在完整请求头被解析、URI 被交给 location 处理之前就被拒绝,那么:

  • access.log 里可能没有该请求;

  • error.log 里可能只有连接级信息;

  • 日志中不一定存在浏览器显示的完整资源 URL。

可以先搜索访问日志:

grep 'styles-d4e5f6.css' /var/log/openresty/example-access.log

判断方式:

  • 没有记录:更支持请求在 HTTP/2 流或连接层被拒绝;

  • 200:Nginx 已处理请求,需要继续检查中间网关、网络设备或浏览器连接;

  • 4044035xx:按对应 HTTP 状态码检查资源路径、权限或服务错误。

为了让访问日志包含协议和连接信息,可以增加:

log_format h2_detail
    '$remote_addr [$time_local] "$request" '
    'status=$status protocol=$server_protocol h2=$http2 '
    'connection=$connection connection_requests=$connection_requests '
    'request_time=$request_time';

access_log /var/log/openresty/example-access.log h2_detail;

八、检查最终生效配置,而不是只看一个文件

OpenResty 配置通常会包含多个目录:

include /usr/local/openresty/nginx/conf/conf.d/*.conf;
include /usr/local/openresty/nginx/conf/default/*.conf;
include /etc/openresty/waf.conf;

因此,需要查看合并后的有效配置:

/usr/local/openresty/nginx/sbin/nginx -T 2>&1 |
grep -nE 'keepalive_timeout|keepalive_requests|http2|max_concurrent|limit_conn|limit_req'

重点检查:

  1. 是否有某个 serverlocation 再次覆盖为 keepalive_timeout 0

  2. WAF 或其他 include 是否设置了较小的 limit_connlimit_req

  3. 是否异常调低了 http2_max_concurrent_streams

  4. 服务是否在故障期间频繁 reload、重启或退出 worker。

下面两行只负责创建限流共享内存区,本身不会执行限流:

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

只有其他配置实际使用 limit_conn 时才会限制连接数量,因此仍需检查所有 include 文件。

九、验证修复结果

首先确认 HTTPS 仍然协商为 HTTP/2,而不是因为关闭 HTTP/2 才恢复。

在 Chrome 开发者工具 Network 面板中显示 Protocol 列:

  • h2:正在使用 HTTPS + HTTP/2;

  • http/1.1:HTTPS 正常,但当前没有使用 HTTP/2。

也可以使用 curl 验证协议:

curl -skI --http2 \
  https://app.example.com:8443/assets/styles-d4e5f6.css

单个 curl 请求只能验证 HTTP/2 协商和基本响应,不能完全模拟浏览器同时加载几十个资源。因此最终仍应使用浏览器连续多次硬刷新,并观察:

  • Network 中协议是否为 h2

  • 所有 JS/CSS 是否返回 200304

  • 是否仍有 ERR_HTTP2_SERVER_REFUSED_STREAM

  • OpenResty 是否发生 reload、worker 退出或连接异常。

十、结论

这次问题与 Vue/Vite、HTTPS 证书以及 gzip 兼容性无关。OpenResty 1.27.1.2 支持 HTTPS + HTTP/2,实际故障来自不合适的连接配置:

keepalive_timeout 0;

HTTP/2 依赖一条持久连接复用多个 stream。禁用 keep-alive 后,页面后续动态加载的资源流可能遇到连接关闭或 GOAWAY,最终被 Chrome 报告为 ERR_HTTP2_SERVER_REFUSED_STREAM

将其恢复为合理的非零值即可:

keepalive_timeout 75s;
keepalive_requests 1000;

这次排查最值得记住的经验有三点:

  1. HTTPS 正常建立不代表 HTTP/2 连接生命周期配置合理;

  2. 前端大量 chunk 往往只是放大了连接层问题;

  3. HTTP/2 流在进入请求处理前被拒绝时,日志里未必有对应 URL。

参考资料

0

评论区