是否可以替换StreamingResponse中的超链接?
我正在使用以下代码来流式传输HTML内容。
from starlette.requests import Request
from starlette.responses import StreamingResponse
from starlette.background import BackgroundTask
import httpx
client = httpx.AsyncClient(base_url="http://containername:7800/")
async def _reverse_proxy(request: Request):
url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
rp_req = client.build_request(
request.method, url, headers=request.headers.raw, content=await request.body()
)
rp_resp = await client.send(rp_req, stream=True)
return StreamingResponse(
rp_resp.aiter_raw(),
status_code=rp_resp.status_code,
headers=rp_resp.headers,
background=BackgroundTask(rp_resp.aclose),
)
app.add_route("/titles/{path:path}", _reverse_proxy, ["GET", "POST"])
它工作正常,但我想更换a href
链接
有可能吗?
我已经厌倦了像下面这样包装发电机:
async def adjust_response(iterable):
# Adjust hyperlinks in response.
async for element in iterable.aiter_raw():
yield element.decode("utf-8").replace("/admin", "/gateway/admins/SERVICE_A").encode("utf-8")
但这导致了错误:
h11._util.LocalProtocolError: Too much data for declared Content-Length
一个解决方案显然是读取原始响应生成器(如上面注释部分所述(,修改每个href
链接,然后生成修改后的内容。
另一种解决方案是使用JavaScript查找HTML文档中的所有链接,并相应地对其进行修改。如果您可以访问外部服务的HTML文件,则只需添加一个脚本来修改所有href
链接,前提是Window.location
没有指向服务的主机(例如if (window.location.host != "containername:7800" ) {...}
(。即使您不能访问外部HTML文件,您仍然可以在服务器端访问。您可以创建一个StaticFiles
实例来为replace.js
脚本文件提供服务,只需在HTML页面的<head>
部分中使用<script>
标记注入该脚本(注意:如果没有提供<head>
标记,则查找<html>
标记并创建包含<script>
的<head></head>
(。当使用window.onload
事件加载了整个页面时,或者最好在使用DOMContentLoaded
事件完全加载和解析了初始HTML文档时(无需等待样式表、图像等完成加载(,可以运行脚本。使用这种方法,您不必在服务器端遍历每个区块来修改每个href
链接,而是注入脚本,然后在客户端进行替换。
顺便说一句,如果传入的请求有一个相当大的主体,无法放入RAM(例如,如果请求中包含大型文件(,并且会导致应用程序速度减慢甚至崩溃,那么与其使用await request.body()
将整个主体读取到RAM中,不如使用Starlette的stream()
方法将其分块读取(请参阅此答案和此答案(,它返回一个CCD_ 19字节生成器(参见CCD_ 20的流请求文档(;因此,您可以使用:client.build_request(..., content=request.stream())
。
工作示例:
# ...
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static-js", StaticFiles(directory="static-js"), name="static-js")
client = httpx.AsyncClient(base_url="http://containername:7800/")
async def iter_content(r):
found = False
async for chunk in r.aiter_raw():
if not found:
idx = chunk.find(bytes('<head>', 'utf-8'))
if idx != -1:
found = True
b_arr = bytearray(chunk)
b_arr[idx+6:] = bytes('<script src="/static-js/replace.js"></script>', 'utf-8') + b_arr[idx+6:]
chunk = bytes(b_arr)
yield chunk
async def _reverse_proxy(request: Request):
url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
rp_req = client.build_request(
request.method, url, headers=request.headers.raw, content=await request.body()
)
rp_resp = await client.send(rp_req, stream=True)
return StreamingResponse(
iter_content(rp_resp),
status_code=rp_resp.status_code,
headers=rp_resp.headers,
background=BackgroundTask(rp_resp.aclose),
)
app.add_route("/titles/{path:path}", _reverse_proxy, ["GET", "POST"])
JS脚本(replace.js
(:
document.addEventListener('DOMContentLoaded', (event) => {
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++) {
let path = anchors[i].pathname.replace('/admin', '/admins/SERVICE_A');
anchors[i].href = path + anchors[i].search + anchors[i].hash;
}
});