프로그래밍/Python

[ FastAPI ] Response 구조 분석 및 공통 구조 커스텀하기

코줍 2025. 2. 27. 23:57

미들웨어 적용도 해볼 겸 API response 구조를 통일해보려 했다.
문서를 보니, 미들웨어 적용 자체는 꽤 간단했다. ( response 내부 까보기 전까진..ㅎ )
 
https://fastapi.tiangolo.com/tutorial/middleware/#create-a-middleware

 

Middleware - FastAPI

FastAPI framework, high performance, easy to learn, fast to code, ready for production

fastapi.tiangolo.com

 

pydantic으로 BaseResponse 클래스 만들고 적용 

base_response.py

class BaseResponse(BaseModel):
    """공통 response DTO"""

    status_code: int
    data: Any
    message: str = "success"

 
main.py

@app.middleware("http")
async def apply_base_response(request: Request, call_next):
    response = await call_next(request)
    return BaseResponse(
        status_code=response.status_code,
        data=response,
    )

TypeError: 'BaseResponse' object is not callable 오류

 
해당 오류가 발생하는 이유는 FastAPI가 기대하는 Response 객체가 아니기 때문이다. 
그렇다면, FastAPI가 기대하는 Response 객체의 형태는 무엇일까?

Starlette의 Response 구조를 따르는 FastAPI

FastAPI는 기본적으로 starlette의 response구조를 따른다.

@app.middleware("http")
async def apply_base_response(request: Request, call_next):
    response = await call_next(request)
    status_code = response.status_code
    body = response.body

    return JSONResponse(
        content=BaseResponse(status_code=status_code, data=body).model_dump(),
        status_code=status_code,
    )

 
starlette의 response구조대로 맞춰서 수정해보았다. 

AttributeError: '_StreamingResponse' object has no attribute 'body' 오류

 
위와 같이 body를 참조해서 가져올수가 없는 형태이다.
 
왜 그럴까?

StreamingResponse

starlette의 response 구조다.

class StreamingResponse(Response):
    def __init__(self, content: AsyncIterable[bytes], status_code: int = 200):
        self.body_iterator = content
        self.status_code = status_code

 
 
보면 body를 chunk단위로 쪼개서 전달하고 있다.
그래서 .body가 아닌  body_iterator를 통해서 response 전문에 해당하는 내용은 iterable한 데이터를 content필드에 넘겨줘야 한다.

Request 객체 내 body 까보기 

다시 FastApi로 돌아와서 Request객체 내 body를 까보았다.
어떤식으로 content를 넘겨야 할지 파악하기 위해서다.

async def body(self) -> bytes:
    if not hasattr(self, "_body"):
        chunks: list[bytes] = []
        async for chunk in self.stream():
            chunks.append(chunk)
        self._body = b"".join(chunks)
    return self._body

 

  1. body값은 비동기 스트리밍 방식으로 읽고, _body에 캐싱을 한다.
  2. _body값에서는 stream()을 통해 요청 본문을 읽은 chunk들을 합쳐서 저장된다.

따라서 아래와 같이 코드를 수정하고, 미들웨어를 활용하여 API response를 통일된 커스텀 구조로 처리하니 정상적으로 동작하는 것을 확인할 수 있었다.

@app.middleware("http")
async def apply_base_response(request: Request, call_next):
    response = await call_next(request)
    status_code = response.status_code

    body = b""
    async for chunk in response.body_iterator:
        body += chunk
    try:
        body = json.loads(body)
    except json.JSONDecodeError:
        body = body.decode()

    return JSONResponse(
        content=BaseResponse(status_code=status_code, data=body).model_dump(),
        status_code=status_code,
    )