FastAPI子应用嵌入iframe时混合内容错误的排查与解决
我在Google Cloud Run上部署了基于FastAPI的NiceGUI Web应用,主应用通过HTTPS提供服务,但将Chainlit子应用以iframe形式嵌入主应用(Chainlit已挂载为FastAPI子应用)。加载主应用时出现混合内容错误,Chainlit端点无法加载,错误提示:
Mixed Content: The page at 'https://main-app.a.run.app/' was loaded over HTTPS, but requested an insecure frame 'http://main-app.a.run.app/chat/'. This request has been blocked; the content must be served over HTTPS.
手动在浏览器调试工具修改iframe URL再改回后,iframe能正常加载。请问:导致子应用默认使用HTTP的原因是什么?如何确保其始终通过HTTPS提供服务以避免混合内容错误?
我的实现代码如下:
from nicegui import context, app, ui import chainlit as cl from chainlit.utils import mount_chainlit ui.html(f'<iframe id="chat_frame" src="{CHAINLIT_BASE_URL}" style="width: 100%; height: 100%; border: none;"></iframe>').classes('w-full h-full') @app.middleware("http") async def add_security_headers(request, call_next): response = await call_next(request) # Content Security Policy header for iframe embedding response.headers['Content-Security-Policy'] = f"frame-ancestors 'self' {URL_REMOTE};" return response mount_chainlit(app=app, target="chainlit_app.py", path="/chat") # Mount the Chainlit app ui.run(title=TITLE, favicon=FAVICON, storage_secret=API_KEY, host=BASE_URL, port=PORT)
原因分析
- Google Cloud Run内部通过反向代理与应用通信,应用接收到的请求协议是HTTP,Chainlit默认读取这个内部协议生成资源URL,而非外部用户访问的HTTPS,导致iframe请求HTTP资源。
- 如果
CHAINLIT_BASE_URL是硬编码的HTTP地址,或者没有根据当前请求动态生成,也会直接引发混合内容问题。 - 手动修改URL再改回后能加载,是因为浏览器此时会强制沿用当前页面的HTTPS协议重新请求,相当于修正了协议不匹配的问题。
解决方法
配置应用信任反向代理
在ui.run()之前添加中间件,让FastAPI/Starlette识别反向代理传递的协议头,强制所有请求使用HTTPS:from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware # 信任Cloud Run反向代理,允许转发的协议头生效 app.add_middleware(TrustedHostMiddleware, allowed_hosts=["*"]) app.state.trusted_proxies = ["*"] # 生产环境建议指定Cloud Run的代理IP段,提升安全性 app.add_middleware(HTTPSRedirectMiddleware) # 自动将HTTP请求重定向到HTTPS动态生成Chainlit的HTTPS地址
避免硬编码CHAINLIT_BASE_URL,从当前请求上下文动态获取协议和主机地址:@ui.page('/') def main_page(): # 从NiceGUI的请求上下文获取当前协议和主机 request = context.get_client().request chainlit_url = f"{request.url.scheme}://{request.url.hostname}/chat" ui.html(f'<iframe id="chat_frame" src="{chainlit_url}" style="width: 100%; height: 100%; border: none;"></iframe>').classes('w-full h-full')配置Chainlit的服务地址
通过环境变量强制Chainlit使用HTTPS地址:
在Cloud Run部署配置中添加环境变量:CHAINLIT_SERVER_URL=https://main-app.a.run.app/chat或者在代码中动态设置:
import os request = context.get_client().request os.environ["CHAINLIT_SERVER_URL"] = f"{request.url.scheme}://{request.url.hostname}/chat"完善Content-Security-Policy头
更新CSP规则,明确允许HTTPS的iframe资源:@app.middleware("http") async def add_security_headers(request, call_next): response = await call_next(request) host = request.url.hostname # 允许当前域名的HTTPS iframe资源 response.headers['Content-Security-Policy'] = f"frame-ancestors 'self' {URL_REMOTE}; frame-src 'self' https://{host};" return response
内容的提问来源于stack exchange,提问作者John Szatmari

