claude / app.py
yangtb2024's picture
feat: 格式化记录 JSON POST 请求体
2e4a225
import requests
from fastapi import FastAPI, Request, Response
from fastapi.responses import StreamingResponse
import uvicorn
import os
import logging # 添加 logging 导入
import json # 添加 json 导入
# 配置日志记录
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = FastAPI()
# 允许的目标域名列表(可选,增加安全性)
# ALLOWED_HOSTS = {"google.com", "example.com"}
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def reverse_proxy(request: Request, path: str):
"""
反向代理到目标 URL。
从路径中提取目标 URL,例如 /google.com/search?q=test -> https://google.com/search?q=test
"""
target_url_str = path
# 简单的检查,确保路径看起来像一个域名或包含协议
if not ('.' in target_url_str or target_url_str.startswith(('http://', 'https://'))):
return Response(content="Invalid target URL format in path.", status_code=400)
# 如果路径不包含协议,默认添加 https://
if not target_url_str.startswith(('http://', 'https://')):
target_url_str = f"https://{target_url_str}"
# 提取域名进行检查(可选)
# try:
# from urllib.parse import urlparse
# parsed_url = urlparse(target_url_str)
# if parsed_url.netloc not in ALLOWED_HOSTS:
# return Response(content=f"Host not allowed: {parsed_url.netloc}", status_code=403)
# except Exception:
# return Response(content="Could not parse target URL.", status_code=400)
# 准备目标请求的 headers,复制客户端 headers,但移除 Host
headers = {key: value for key, value in request.headers.items() if key.lower() != 'host'}
# 可以选择性地添加或修改 headers
# headers['X-Forwarded-For'] = request.client.host
# 获取请求体
body = await request.body()
# 如果是 POST 请求,记录请求体内容
if request.method == "POST":
logging.info(f"Received POST request to {path}. Body follows:")
try:
# 尝试解码为 UTF-8 文本
body_text = body.decode('utf-8')
try:
# 尝试将文本解析为 JSON
body_json = json.loads(body_text)
# 如果成功,格式化 JSON 并记录
formatted_json = json.dumps(body_json, indent=2, ensure_ascii=False) # indent=2 用于缩进,ensure_ascii=False 支持中文
logging.info(formatted_json)
except json.JSONDecodeError:
# 如果不是有效的 JSON,按原样记录文本(已包含换行处理)
logging.info(body_text)
except UnicodeDecodeError:
# 如果解码失败,记录原始字节信息
logging.info(f"Body (bytes): {body}")
try:
# 发送请求到目标服务器,允许重定向,不验证 SSL 证书(在某些情况下可能需要)
target_response = requests.request(
method=request.method,
url=target_url_str,
headers=headers,
data=body,
stream=True, # 使用流式传输处理大文件或长时间响应
allow_redirects=True, # 允许目标服务器重定向
verify=True # 通常应保持 True,除非你知道目标证书有问题
)
# 过滤掉不应转发的响应头 (例如 'transfer-encoding', 'content-encoding', 'content-length')
response_headers = {
k: v for k, v in target_response.headers.items()
if k.lower() not in ['transfer-encoding', 'content-encoding', 'content-length']
}
# 使用 StreamingResponse 将目标响应流式传输回客户端
return StreamingResponse(
target_response.iter_content(chunk_size=8192),
status_code=target_response.status_code,
headers=response_headers,
media_type=target_response.headers.get('content-type')
)
except requests.exceptions.RequestException as e:
return Response(content=f"Error connecting to target server: {e}", status_code=502) # Bad Gateway
except Exception as e:
return Response(content=f"An unexpected error occurred: {e}", status_code=500)
if __name__ == "__main__":
port = int(os.environ.get("PORT", 7860)) # Hugging Face Spaces 通常使用 7860 端口
uvicorn.run(app, host="0.0.0.0", port=port)