目录
- 一,问题背景
- 二,探索问题的过程
- 三,问题的解释
- 四,解决方案
一,问题背景
最近在做 Python FastAPI 和 Java spring boot 对接接口的事情。
FastAPI 接口写好后,postman 和 swage 测试都正常,但是 Java 服务使用HttpExchange怎么着都请求 400,参数不合法。
检查了请求参数,看着也没问题,也都有值。
Python 服务经过 debug 日志发现,请求体是空:
@app.post("/test")asyncdefaudit_product_raw_debug(request:Request):# 打印原始 body(bytes)body_bytes=awaitrequest.body()logger.info(f"Raw request body (bytes):{body_bytes}")# 尝试 decode 为字符串try:body_str=body_bytes.decode('utf-8')logger.info(f"Raw request body (str):{body_str}")exceptExceptionase:logger.error(f"Failed to decode body:{e}")# 尝试手动解析 JSON(用于调试)try:importjson body_json=json.loads(body_str)logger.info(f"Parsed JSON:{body_json}")exceptExceptionase:logger.error(f"Failed to parse JSON:{e}")# 然后继续走正常逻辑(或返回)return{"message":"debug only"}二,探索问题的过程
我写一个极其简单的 fast api demo :uv add fastapi uvcorn
# main.py from fastapi import FastAPI from pydantic import BaseModel # 创建 FastAPI 应用 app = FastAPI() # 定义一个请求体的 Pydantic 模型,表示接收的数据 class DataRequest(BaseModel): data: str # 假设这是你的 Python 逻辑代码 def process_data(data): return {"message": f"Processed data: {data}"} # 定义一个 POST 请求的接口 @app.post("/process") async def process(request: DataRequest): # 调用你的 Python 业务逻辑 result = process_data(request.data) return result if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="192.168.0.103", port=8000, reload=True)和一个简单的 Java 客户端请求:
publicclassApp{publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{// 创建 HttpClientHttpClientclient=HttpClient.newHttpClient();// 构造请求体StringjsonRequest="{\"data\": \"Hello, FastAPI!\"}";// 创建 POST 请求HttpRequestrequest=HttpRequest.newBuilder().uri(URI.create("http://192.168.0.103:8000/process")).header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString(jsonRequest)).build();// 发送请求并接收响应HttpResponse<String>response=client.send(request,HttpResponse.BodyHandlers.ofString());// 输出响应System.out.println("Response status code: "+response.statusCode());System.out.println("Response body: "+response.body());// Response status code: 400//Response body: Invalid HTTP request received.}}经过测试发现,Python web 可以正常获取到 Java 客户端的请求,而Java 客户端也可以正常请求到 Python 的 http 响应。
但关键来了,当 Python 处理 Java 请求时,打印了这么一个日志:
WARNING: Unsupported upgrade request. WARNING: No supported WebSocket library detected. Please use "pip install 'uvicorn[standard]'", or install 'websockets' or 'wsproto' manually.于是我进行了如下测试:
测试 1:执行uv add 'uvicorn[standard]',运行 Python web 服务,然后 Java 客户端测试,结果真的复现了前面我提到的错误,Python 请求体是空的,日志:
WARNING: Unsupported upgrade request. WARNING: Invalid HTTP request received. INFO: 192.168.0.103:60011 - "POST /process HTTP/1.1" 422 Unprocessable ContentJava 报错:
Response status code: 422 Response body: {"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null}]}测试 2:uv remove 'uvicorn[standard]', uv add uvicorn , 重启 Python,Java客户端请求正常;
测试 3:uv add websockets,重启 Python,Java客户端请求正常。
三,问题的解释
关键日志应该是这个:
WARNING: Unsupported upgrade request. WARNING: No supported WebSocket library detected. Please use "pip install 'uvicorn[standard]'", or install 'websockets' or 'wsproto' manually.Java 客户端发送请求时,会尝试使用 HTTP/2 协议,于是请求协议升级;而 uvicorn[standard] 的 http_tools 解析工具 将其视为 “不支持的协议升级请求” ,于是丢弃了请求体。
而 uvicorn 使用 h11 解析器,兼容性更好,依然保留了请求体。
下面通过抓包工具,看看 Java 请求头:
然后看看 postman 为什么能访问,它就没有 upgrade 请求头:
但是奇怪的是,postman 加了 upgrade 请求头依然能正常访问,可能 Java 底层 和 postman 底层还是有什么区别吧。
四,解决方案
方式1,Python 服务,使用 uvicorn + websockets(可选),而不是用 uvicorn[standard]
方式2,如果你想使用 uvicorn[standard] 高性能 http ,可以在 uvicorn 中强制使用 http1.1 ,这样 uvicorn[standard] 会直接忽略升级操作
uvicorn.run("app.main:app",host=settings.service_ip,port=settings.service_port,reload=settings.debug,log_level=settings.log_level.lower(),http="h11")方式3:Java 禁用升级请求头。但具体怎么操作,我还没有弄清楚,不同的客户端使用方式也不一样。至少,我在 Java 原生的 HttpClient 上强制 http1.1 ,还是没效果,还是发送了升级请求。
推荐使用方式 2 ,这个最简单。