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

推荐订阅源

Cyberwarzone
Cyberwarzone
V
Vulnerabilities – Threatpost
T
Tenable Blog
Forbes - Security
Forbes - Security
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY
Know Your Adversary
Know Your Adversary
S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
V
Visual Studio Blog
WordPress大学
WordPress大学
Latest news
Latest news
K
Kaspersky official blog
T
Tailwind CSS Blog
T
Threat Research - Cisco Blogs
B
Blog RSS Feed
C
Cisco Blogs
博客园 - 聂微东
Martin Fowler
Martin Fowler
T
The Blog of Author Tim Ferriss
小众软件
小众软件
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
L
LINUX DO - 热门话题
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Privacy International News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
S
SegmentFault 最新的问题
Security Latest
Security Latest
Y
Y Combinator Blog
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
P
Privacy & Cybersecurity Law Blog
L
LINUX DO - 最新话题
月光博客
月光博客
The GitHub Blog
The GitHub Blog
博客园 - 三生石上(FineUI控件)
S
Security Affairs
P
Proofpoint News Feed
D
DataBreaches.Net
有赞技术团队
有赞技术团队
云风的 BLOG
云风的 BLOG

est の 输入 输出和出入

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 FSRS核心字段 SVG 时钟 AI的自我觉醒是必然 唐宋之变 Louis Alexander 谈英语学习 Predict Next Word Instruct 的意义 Sentry 替代品 Bugsink 安装配置 浏览器通过WebGPU上做AI推理 Indent Is All You Need 语文学习和考试 失落的学习兴趣 如何跟孩子讲学习的意义 物权 Bonsai 在 M2 安装 The Porsche Diffusion 首页和404更新 无用之用——或许LLM真的还不是AGI 发明后训练的人真是天才 看好 Taalas 码奸 claw会代替员工? 如果拿 token 抵工资会发生什么? tmux enables AIs to operate servers safely chat.est.im launches at 3.14 2026 股灾? AI 弑父 Configurable Intelligence 巴别塔,Vibe Coding和Lisp之咒 Citrini的2028智力危机 中文不是唯一的意译语言 观星有感 体细胞阶级论——当繁衍权被剥夺
不修改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居然是二进制流。