FastAPI微服务间用aiohttp上传多文件报422错误排查
问题:使用aiohttp跨FastAPI服务传文件返回422(Unprocessable Entity)错误
问题背景
在微服务架构中,尝试通过aiohttp从一个FastAPI服务向另一个FastAPI服务发送文件上传请求,使用requests库时正常,但切换到aiohttp客户端后出现422错误。
相关代码
第一个服务代码
from __future__ import annotations import json from typing import Dict from aiohttp import ClientSession, TCPConnector, FormData from fastapi import FastAPI, File, UploadFile, Annotated class HttpClient: session: ClientSession = None headers = {'Content-Type': 'application/json', 'Accept': 'application/json'} @staticmethod async def start( *args, headers=None, json_serialize=lambda obj, **kwargs: json.dumps(obj, default=str, **kwargs), raise_for_status=True, connector_parameters: Dict = None, **kwargs ): if headers is None: headers = HttpClient.headers if connector_parameters is None: connector_parameters = {} connector = TCPConnector(**connector_parameters) HttpClient.session = ClientSession(headers=headers, json_serialize=json_serialize, raise_for_status=raise_for_status, connector=connector, **kwargs) @staticmethod async def stop(): await HttpClient.session.close() HttpClient.session = None @staticmethod async def restart(*args, **kwargs): await HttpClient.stop() await HttpClient.start(*args, **kwargs) def __call__(self) -> ClientSession: assert HttpClient.session is not None, "Session not found. Please start or restart session." return HttpClient.session client = HttpClient() app = FastAPI() async def call_second_service(files: list[UploadFile]): try: data = FormData() for file in files: data.add_field('files', file.file, filename=file.filename, content_type=file.content_type) async with client().post(url="http://localhost:8003/upload", data=data) as response: response_file = await response.read() return response_file except Exception as e: raise e @app.on_event("startup") async def startup(): await client.start() @app.on_event("shutdown") async def shutdown(): await client.stop() @app.post('/upload') async def upload(files: Annotated[list[UploadFile], File(description="Upload files")]): return await call_second_service(files)
第二个服务上传端点代码
from fastapi import FastAPI, File, UploadFile, Annotated app = FastAPI() @app.post("/upload") async def upload_file(files: Annotated[list[UploadFile], File(description="Files")]): # 业务逻辑 return {"message": "Files received"}
错误信息
ERROR: Exception in ASGI application Traceback (most recent call last): File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 411, in run_asgi result = await app( # type: ignore[func-returns-value] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 69, in __call__ return await self.app(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/fastapi/applications.py", line 1054, in __call__ await super().__call__(scope, receive, send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/applications.py", line 123, in __call__ await self.middleware_stack(scope, receive, send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 186, in __call__ raise exc File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__ await self.app(scope, receive, _send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 62, in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app raise exc File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app await app(scope, receive, sender) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/routing.py", line 758, in __call__ await self.middleware_stack(scope, receive, send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/routing.py", line 778, in app await route.handle(scope, receive, send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/routing.py", line 299, in handle await self.app(scope, receive, send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/routing.py", line 79, in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app raise exc File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app await app(scope, receive, sender) File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/starlette/routing.py", line 74, in app response = await func(request) ^^^^^^^^^^^^^^^^^^^ File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/fastapi/routing.py", line 278, in app raw_response = await run_endpoint_function( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/fastapi/routing.py", line 191, in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/furkankyildirim/Desktop/ocr-gateway/src/__init__.py", line 78, in upload return await call_second_service(files) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/furkankyildirim/Desktop/ocr-gateway/src/__init__.py", line 63, in call_second_service raise e File "/Users/furkankyildirim/Desktop/ocr-gateway/src/__init__.py", line 59, in call_second_service async with client().post(url="http://localhost:8003/upload", data=data) as response: File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/aiohttp/client.py", line 1194, in __aenter__ self._resp = await self._coro ^^^^^^^^^^^^^^^^ File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/aiohttp/client.py", line 693, in _request resp.raise_for_status() File "/Users/furkankyildirim/Desktop/ocr-gateway/venv/lib/python3.11/site-packages/aiohttp/client_reqrep.py", line 1060, in raise_for_status raise ClientResponseError( aiohttp.client_exceptions.ClientResponseError: 422, message='Unprocessable Entity', url=URL('http://localhost:8003/upload')
错误原因
核心问题出在HttpClient类的默认headers设置:
- 默认headers指定了
Content-Type: application/json,而文件上传需要使用multipart/form-data格式。 - 当使用aiohttp发送
FormData时,框架本会自动生成包含正确boundary的multipart/form-dataContent-Type,但全局默认的application/json会覆盖这个自动生成的header。 - 第二个FastAPI服务收到请求后,发现Content-Type是json,但实际请求体是multipart格式,无法解析,因此返回422错误。
而requests库在发送文件时会自动处理Content-Type,不会被全局header干扰,所以能正常工作。
解决方案
有两种修复方式:
方式一:修改HttpClient默认headers
去掉默认的Content-Type,只保留Accept,让aiohttp根据请求内容自动设置正确的Content-Type:
class HttpClient: session: ClientSession = None # 移除Content-Type,只保留Accept headers = {'Accept': 'application/json'}
方式二:发送请求时临时覆盖headers
在post请求中传入headers参数,临时去掉Content-Type:
async def call_second_service(files: list[UploadFile]): try: data = FormData() for file in files: data.add_field('files', file.file, filename=file.filename, content_type=file.content_type) # 临时覆盖headers,移除Content-Type async with client().post( url="http://localhost:8003/upload", data=data, headers={'Accept': 'application/json'} ) as response: response_file = await response.read() return response_file except Exception as e: raise e
两种方式都能让aiohttp自动生成正确的multipart/form-data Content-Type,确保第二个服务能正确解析文件上传请求。
内容的提问来源于stack exchange,提问作者furkankyildirim
相关产品推荐
相关产品推荐

