惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
人人都是产品经理
人人都是产品经理
博客园 - 聂微东
博客园_首页
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
小众软件
小众软件
F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
量子位
Google DeepMind News
Google DeepMind News
Jina AI
Jina AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
宝玉的分享
宝玉的分享
有赞技术团队
有赞技术团队
J
Java Code Geeks
WordPress大学
WordPress大学
The Cloudflare Blog

est の 输入 输出和出入

海南之行后记 The httpx 1.0 situation Mimocode/Opencode 的 Responses API 支持 git fetch 实现断点续传 继续搓英汉双解词典v3 邮箱生态调研2026 “畜牲” 飞书并入豆包,文档是否减少? 从菲尔兹奖谈「包养」 iosevka字体让中文 ASCII diagram 图表对齐 为什么 Github OAuth 故意拦截 CORS gitweets改版,复刻微信「朋友圈」 MiMoCode 干完活儿发通知 写作能力和 locate cost grep vs sqlite 谁更适合微信聊天记录? [AI] curl -NT. 导致100% CPU原因 或许「数羊」真是个有效的入睡法 唯物主义「天命」论 我的 Vibe Coding 最佳实践——ADR文档 MacOS 快速插入当前时间 locate cost 基于 git 的零拷贝静态web服务器 AI和柜台费 Sutton 论 discovery Elon Musk 五步工作法 Playlet:DLNA听歌神器 免安装app播放NAS里的歌 AI 硬伤 理解LLM的范式——它就是个差分机? 路径依赖 AI 流式接口的pattern
不修改nginx接收websocket
est · 2026-05-28 · via est の 输入 输出和出入

以前只记得要支持websocket 需要 nginx 改:

  location /ws-service-a {
      proxy_pass http://backend_a;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
  }

今天折腾忘记这茬了,一直在调 FastAPI ,@app.websocket() 死活不识别 wscat 的请求

WebSocket 协议的工作方式是:

1.客户端发 GET + Upgrade: websocket 头
2.服务端识别这个特殊 GET,返回 101 Switching Protocols
3.连接升级完成

opus-4.6 说:既然路由确实注册了,那问题几乎可以确定是:前面的反向代理/LB 没有透传 WebSocket

我就来劲了,让AI写一份 特殊 GET,返回 101 Switching Protocols 。

结果还真成功了。wscat 返回 error: Invalid Sec-WebSocket-Accept header

折腾了几下,发现此路不通

问题不是 @app.websocket() 没挂载,它挂载了,但永远匹配不到。原因是 uvicorn 的路由分发发生在更底层:

请求到达 uvicorn

uvicorn 检查 headers:有 Upgrade: websocket?
├── 是 → scope.type = "websocket" → 只匹配 @app.websocket() 路由
└── 否 → scope.type = "http" → 只匹配 @app.get/post() 路由
你的 nginx 把 Upgrade 头吃了,所以 uvicorn 永远把请求标记为 http scope。在 http scope 下,@app.websocket(...) 路由根本不参与匹配 — 就像它不存在一样。

这就是为什么:

@app.get('/ws/test') 能匹配 — 它是 HTTP 路由,匹配 HTTP scope
@app.websocket('/ws/real') 匹配不到 — 它是 WebSocket 路由,只在 websocket scope 下才可见
两个都注册成功了,但 scope 类型不对,uvicorn 在路由匹配之前就把它们分到了不同的赛道。

接下来几轮错误复制粘贴大法:

  • 最初想法:ASGI middleware 翻译协议 → 80 行,手搓 websockets 库
  • 精简版:用 websockets 库做帧编码 → 还是 50 行 middleware

后来发现ASGI 这一层做不到。因为 标准 ASGI middleware 拿到请求时已经太晚了:

uvicorn TCP 收包
1. httptools 解析 headers
2. on_headers_complete 决定 scope type (http vs websocket) ← 需要在这里拦截
3. 创建 scope + receive/send(已经绑定为 HTTP 协议)
4. 调用 ASGI app (middleware 链) ← middleware 才在这里介入

middleware 只能看到已经定型的 scope['type'] = 'http',改不了底层的 receive/send 绑定。

然后尝试了一个 gunicorn.conf.py 的hack:

def post_worker_init(worker):
    """让 uvicorn 从 sec-websocket-key 识别 WebSocket,绕过 nginx 吞 Upgrade 头的问题"""
    import httptools
    from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
    _orig = HttpToolsProtocol.on_headers_complete
    def _patched(self):
        has_ws_key = any(n == b"sec-websocket-key" for n, _ in self.headers)
        if has_ws_key and self._should_upgrade_to_ws():
            self.headers.append((b"upgrade", b"websocket"))
            self.headers.append((b"connection", b"Upgrade"))
            self.scope["headers"] = self.headers
            self.scope["method"] = self.parser.get_method().decode("ascii")
            raise httptools.HttpParserUpgrade(b"")
        return _orig(self)
    HttpToolsProtocol.on_headers_complete = _patched

我也觉得,ws这协议是不是有病。如果有 sec-websocket-key 就认定为 ws 不就完了。搞那么复杂。

然后这个办法在 ASGI 里还是行不通。最终版:直接篡改 uvicorn 收到的 raw TCP 字节

def post_worker_init(worker):
    import re
    from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol

    _orig_data_received = HttpToolsProtocol.data_received

    def _patched_data_received(self, data):
        if not getattr(self, '_ws_patched', False) and b'\r\n\r\n' in data:
            self._ws_patched = True
            lower = data.lower()
            if b'sec-websocket-key:' in lower and b'\nupgrade:' not in lower:
                data = re.sub(rb'(?i)\r\nconnection:[^\r]*', b'\r\nConnection: Upgrade', data)
                data = data.replace(b'\r\n\r\n', b'\r\nUpgrade: websocket\r\n\r\n', 1)
        return _orig_data_received(self, data)

    HttpToolsProtocol.data_received = _patched_data_received

居然成功了!不修改nginx兼容websocket!

这路子太野了。还是老老实实去改nginx了。

不过也学到一些姿势,比如 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ,以及ws居然是二进制流。