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 |