You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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-data Content-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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 01:54:50