FastAPI/Starlette+Requests实现反向代理
Posted | stdout
因为某些拉垮的业务需要,不得不在代码里去反向代理别的 HTTP API
一般格式如下:
@app.get('/other/{other_path:path}')
@app.post('/other/{other_path:path}')
async def other_api(other_path: str, req: Request):
"""透传 API"""
host = 'http://example.intranet'
url = '{}/other/{}'.format(host, other_path)
body = bytes(await req.body()) or None
r = requests.request(
req.method, url,
headers={
'Cookie': req.headers.get('cookie') or '',
'Content-Type': req.headers.get('Content-Type')},
params=req.query_params, data=body, stream=True,
allow_redirects=False)
h = dict(r.headers)
h.pop('Content-Length', None)
return StreamingResponse(r.raw, headers=h, status_code=r.status_code)
解释一下为啥要这么写:
- 采用
stream=True
+StreamingResponse
,防止上游给你返回一个GB体量的文件把 FastAPI 进程内存撑爆 - 只摘取浏览器的
Cookie
+Content-Type
两个头。别的不给上游。 - 要去掉返回的 Content-Length 头。因为 StreamingResponse 会自己定义返回长度。
这样写基本能跑起来了,但是有2个问题:
- 如果上游返回一个 302/303/307 的跳转,那么很有可能把内网的跳转网址直接返回给浏览器了。因为 HTTP/1.1 的标准 RFC2616 强制规定,
Location
的值必须是一个绝对网址。所以这个时候就冒犯一下这个规定,强行截断:loc = h.pop('Location', '') if loc.startswith(host): h['Location'] = loc[len(host):]
注意这里不要用 .lstrip()
。这方法和你想象的完全不是一回事。它是基于单个字符的挨个替换而不是整个字符串。
2. 实测这种在业务代码实现的「软」反向代理性能堪忧。上游4s左右的整个页面加载时间,反向代理之后就变成了 120s 左右。盲猜是 StreamingResponse
是一个字节一个字节去遍历迭代器?实测并不是。主要的锅还是 Requests
的 .raw
是按照 chunked
一段一段返回的。最好还是准备个 4KB 的 buffer,提高性能。解决方案是把 r.raw
改成 r.raw.stream(4096000)
性能一下就高了。注意这里不要用官方文档提供的 .iter_contents()
或者 .iter_lines()
这两者又会自作聪明的去解析文本编码或者换行符,降低了性能。
Comments