FastAPI ErrorHandling 과 CORS (1) - 문제 파악

2025. 2. 10. 00:02·탐구 생활/FastAPI CORS

Starlette와 FastAPI에서 제공하는 CORSMiddleware와 ExceptionHandler가 함께 작동하지 않는 상황은 꽤 혼란스럽습니다. 이는 REST API 개발자 입장에서 상당히 중요한 이슈로, 많은 FastAPI 사용자들이 다양한 해결책을 논의(link, link, link)해 왔지만 제가 봤을때 명확하고 만족스러운 답이 나오지 않은 상태입니다. 우선 문제의 원인과 그 해법이 만족스럽지 않은 이유를 이야기해보겠습니다.

 

다소 장황한 글이 될 수 있기 때문에 문제 원인과 문제 상황만 파악하고자 하시는 분은 "문제의 원인" 그리고 "문제 상황" 만 보셔도 됩니다.

문제 파악

Starlette  Middlewere chain 

Strlette 는 ASGI Spec 을 준수하며, 그에 따라 Middleware 라는 개념이 적용되어 있습니다. 그리고 이러한 Middleware 들이 요청, 응답 과정에서 연쇄적으로 동작하는 것을 Middleware chain 이라고 합니다.

 

FastAPI application 은 Starlette application 을 더 사용하기 편하게 포장한것이고, 대부분의 주요 철학과 구현은 Starlette에 의존하고 있기 때문에 Sarlette의 Middleware chain 을 분석하겠습니다.

주요 Middleware

Starlette 의 middleware chain 에서 cors 문제가 발생하는 문제를 이해하기위해 다음의 개념을 알아야 한비다.

  • Middleware: 요청이 Starlette 애플리케이션에 도달하기 전과 응답이 반환된 후에 개입하여 처리합니다.
  • CORSMiddleware: 사용자가 설정한 CORS 정책에 따라 preflight 요청을 검증하고 응답에 CORS 헤더를 추가합니다.
  • ExceptionMiddleware: 특정 status_code 혹은 Exception 대해서 어떻게 대응할지 설정합니다.
  • ServerErrorMiddleware: ExceptionMiddleware 에서 처리되지 않은 나머지 Exception 에 대응합니다.
  • ExceptionHandler: 예외가 발생했을 때 이를 처리하는 방법을 정의합니다. ExceptionMiddleware 에 등록됩니다.
  • HttpException: Starlette와 FastAPI에서 정의된 예외 클래스로, 상태 코드와 헤더 정보를 포함할 수 있습니다.

Starlette 코드 분석

이제 직접 Starlette application 에 대한 코드를 보겠습니다. 이를 통해 ExceptionMiddleware 와 ServerErrorMiddleware 는 자동으로 Startlette application 이 최초 요청을 받을때 초기화 되어(Lazy) 이후에는 캐싱되어 재사용되는 구조를 가지고 있다는 사실을 알 수 있습니다. 

 

또한 ExceptionHandler 에 등록된 Exception 에 대한 처리는 ServerErrorMiddleware 에 할당된다는 사실도 파악이 가능합니다.

# starlette/applications.py

class Starlette:
    """Creates an Starlette application."""

    def __init__(...)
    
    # line 79
    def build_middleware_stack(self) -> ASGIApp:
        debug = self.debug
        error_handler = None
        exception_handlers: dict[typing.Any, typing.Callable[[Request, Exception], Response]] = {}

        for key, value in self.exception_handlers.items():
            if key in (500, Exception):
                error_handler = value
            else:
                exception_handlers[key] = value

        middleware = (
        	# outermost
            [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
            + self.user_middleware
            + [Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug)]
            # innermost
        )

        app = self.router
        for cls, args, kwargs in reversed(middleware):
            app = cls(app, *args, **kwargs)
        return app

    # line 108
    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        scope["app"] = self
        if self.middleware_stack is None:
            self.middleware_stack = self.build_middleware_stack()
        await self.middleware_stack(scope, receive, send)
        
    # line 123 
    def add_middleware(
        self,
        middleware_class: _MiddlewareFactory[P],
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> None:
        if self.middleware_stack is not None:  # pragma: no cover
            raise RuntimeError("Cannot add middleware after an application has started")
        self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))

    # line 133
    def add_exception_handler(
        self,
        exc_class_or_status_code: int | type[Exception],
        handler: ExceptionHandler,
    ) -> None:  # pragma: no cover
        self.exception_handlers[exc_class_or_status_code] = handler

 

실제 코드 line 79 에 위치한 build_middleware_stack 함수를 통해 유저가 주입한 exception_handler  의 값에 따라 error_handler 와 exception_handler 가 구분되며, middleware 에 유저가 정의한 user_middleware 를 감싸는 구조로 ServerErrorMiddleware 와 ExceptionMiddleware 가 주입됩니다.

 

실제코드 line 108 에 위치한 __call__ 에서 build_middleware_stack 함수가 호출되며, 이를 통해 미들웨어는 Starlette application 생성 시점이 아니라 최초에 Starlette application 이 요청을 처리하는 시점에 초기화 된다는 것을 알 수 있습니다.


문제의 원인

그렇다면 ExceptionMiddleware, ServerErrorMiddleware, CORSMiddleware 중 어디가 문제인걸까요? 

http status code 500 이나 Exception, 혹은 ExceptionHandler 에서 처리되지 않은 Exception 을 상속한 예외들에 대한 책임은 오롯이 ServerErrorMiddleware 의 것입니다.

 

Outermost 에 ServerErrorMiddleware 가 존재하고 status_code 500 이나 Exception 으로 밖에 감지되지 않는 Exception 은 ServerErrorMiddleware로 바로 전달됩니다. 정확히 말하면, 다른 middleware 에서 처리하지 않고 다음 middleware 를 계속 호출한 끝에 ServerErrorMiddleware 에게 전달됩니다.

그렇기 때문에 요청에서는 CORS 를 검사하지만 Exception 에 의한 응답에는 CORS 관련 헤더를 받을 수 없는 겁니다.


문제 상황

그렇다면 도대체 Exception(ServerError)가 발새할때 CORS 헤더가 반환되지 않는게 왜 문제일까요?

 

1. 클라이언트와의 약속이 깨집니다.

일반적으로 클라이언트는 서버와 약속된 에러코드에 따른 적절한 처리를 구현할 것입니다. 하지만 Exception 이 발생할때 의도치 않은 동작을 서버가 반환하기 때문에 클라이언트단에서 적절히 에러에 대응할 수 없게 됩니다.

 

이는 유저경험에도 치명적이며 클라이언트단에서 에러 원인을 파악할 수 없도록 만들기에 에러 대응 시간을 늦추게 됩니다.

2. 일관적이지 못한 예외처리로 혼란을 초래합니다.

Exception 이 아니라 HttpException 과 WebsocketException 은 ExceptionMiddleware에서 처리되기 때문에 CORSMiddleware 의 처리를 거치게 됩니다. 즉, 오로지 Exception 에 대해서만 CORS 응답 헤더가 추가되지 않는 것이지요. 이러한 Starlette의 동작을 이해하지 못한 개발자는 다소 혼란을 겪을 수 밖에 없습니다.

 

특히 FastAPI 에서 제공되는 HttpException 이 발생할때는 CORS 응답이 제대로 처리되기 때문에 "원인을 알 수 없지만, 종종 서버가 CORS 에러를 만들어낸다" 는 결론에 도달하게 됩니다. 게다가 FastAPI 공식문서에서는 이러한 이슈를 다루고 있지 않기 때문에 더욱더 혼란스러울 뿐입니다.


FastAPI 유저들은 이 문제를 어떻게 대응하고 있는지 "FastAPI ErrorHandling 과 CORS (2) - 대안 파악" 에서 알아보겠습니다.

'탐구 생활 > FastAPI CORS' 카테고리의 다른 글

FastAPI ErrorHandling 과 CORS (3) - 문제 해결  (0) 2025.02.22
FastAPI ErrorHandling 과 CORS (2) - 대안 파악  (1) 2025.02.15
'탐구 생활/FastAPI CORS' 카테고리의 다른 글
  • FastAPI ErrorHandling 과 CORS (3) - 문제 해결
  • FastAPI ErrorHandling 과 CORS (2) - 대안 파악
개발프로브
개발프로브
가볍게, 오랫동안 기록하고 싶은 블로그입니다.
  • 개발프로브
    ProbeHub
    개발프로브
  • 전체
    오늘
    어제
    • 분류 전체보기 (48)
      • 탐구 생활 (39)
        • 개발 탐구 (5)
        • FastAPI CORS (3)
        • FastAPI Log (4)
        • gRPC&Python (4)
        • SpringBoot 파헤치기 (2)
        • Python (5)
        • FastAPI (4)
        • Terraform (8)
        • MSA (0)
        • GraphQL (2)
        • 데이터베이스 (2)
      • 기초 지식 (9)
        • Terraform (2)
        • MSA (5)
        • K8s (2)
  • 블로그 메뉴

    • 링크

      • github
      • stackoverflow
    • 공지사항

    • 인기 글

    • 태그

      springboot
      grpc 벤치마크
      python 불변 객체
      FastAPI
      오블완
      Python
      python monorepo
      sqlalchemy
      msa monorepo
      grpc gateway
      db 계층형
      python msa monorepo
      python monorepo using uv
      계층형 데이터
      rdb 계층형
      spring 트랜잭션
      grpc 이론적 배경
      grpc 왜 빠른가
      how sqlalchemy works
      fastapi cors
      java
      MSA
      how sqlalchemy orm works
      티스토리챌린지
      Terraform
      rest vs grpc
      PostgreSQL
      grpc
      fastapi logging
      트랜잭션 일관성
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    개발프로브
    FastAPI ErrorHandling 과 CORS (1) - 문제 파악
    상단으로

    티스토리툴바