Search

FastAPI CORS 마주하기

OO 님 CORS 에러 확인 부탁드립니다.

애플리케이션 코드를 작성하고 서버를 띄우고 Postman으로 테스트 할때는 동작이 잘 되었습니다. 프론트와 같이 작업을 하니 CORS 에러가 발생하기 시작합니다. 에러를 파악하고 해결합니다.

CORS?

쉽게 설명 된 링크로 설명을 갈음 합니다.
TLDR;
보안을 위해 클라이언트와 서버의 주소가 다를 경우(Cross Origin), 클라이언트에서 보내는 request의 header, method, origin, credential 허용 여부에 대해서 가능하다는 응답을 서버에서 받는 경우에만 브라우저가 클라이언트와 서버 사이를 중계합니다.
CORS 에러를 일으키는 주체는 서버도 아니고 클라이언트도 아니고, 그 사이에 있는 브라우저 입니다. 브라우저가 서버로 클라이언트에서 보낼 요청에 대해 검증 요청을 합니다. 클라이언트에서는 따로 설정이 필요한 부분은 없습니다. (credential은 상황에 따라 상이합니다.)
브라우저가 서버와 통신하며 진행하기 때문에, 대부분은 백엔드 개발자가 서버에서 브라우저가 확인하는 값을 맞춰서 응답을 해주게 하면 됩니다. 모든 요청에 대해서 검증을 하는 것은 아니고 일부 보안 검증이 필요한 요청(unsafe request)에 대해서만 진행이 됩니다.
이렇게 하기 위해 검증이 필요한 요청에 대해서 1. 브라우저는 본 요청을 날리기 전에 pre-flight request(OPTIONS)를 날립니다. 응답을 보고 허용되는 요청인지 확인합니다.
확인 Header
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Max-Age(Optional)
본 요청과 매칭이 안될 경우, CORS 에러를 뱉습니다.
2. 본 요청에 대한 응답 헤더에 Access-Control-Allow-Origin 이 있는지 검사합니다.
없을 경우, CORS 에러를 뱉습니다.

어떻게 CORS 해결하지?

starlette는 CORS middleware를 제공합니다. 공식문서 설명 링크
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [ "*" ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/") async def main(): return {"message": "Hello World"}
Python
복사
CORS Middleware 적용 코드
CORS Middleware 옵션을 설명합니다.
CORSMiddleware( allow_origins=['*'], # list[str], 요청을 받아줄 클라이언트 주소 목록입니다. "*" 이 있으면 모든 주소를 허용합니다. allow_credentials=True, # bool, default False( 자격 증명이 담긴 헤더(Cookie)를 허용 여부입니다. allow_methods=['*'], # list[str], 요청을 받아줄 method 목록입니다. "*" 이 있으면 모든 method를 허용합니다. allow_headers=['*'], # list[str], 요청을 받아줄 header 목록입니다. "*" 이 있으면 모든 header를 허용합니다. )
Python
복사
/starlette/middleware/cors.py 에서 코드 를 확인 할 수 있습니다. 코드 추적은 생략하고 다이어그램으로 동작을 정리합니다.
flowchart TD
		preflight_request --> a1[method is `OPTION` & Header has `Access-Control-Request-Method`]
		a1 --> a2["inject CORS related(from CORS settings) Header to Response Header"]
		a2 --> a3[return Response]
		
Mermaid
복사
preflight request sequence
flowchart TD
		preflight_request[preflight_request has passed CORS policy] -->
 		request --> a0["method is not `OPTION` or Header do not have `Access-Control-Request-Method`"]
		a0 --> a1["API handling"]
		a1 --> a2["inject CORS related Header(Access-Control-Allow-Origin)" to Response]
		a2 --> a3[return Response]
Mermaid
복사
request sequence

FastAPI CORS 미들웨어 동작 확인 방법

저는 서버 동작을 할 때, AWS Secret Manager로 환경변수를 주입했습니다. Secret Manager 문자 입력에 * 가 되지 않아 “*” 로 해놓았는데, 환경변수에도 따옴표가 함께 들어가서 동작이 안되는 경우가 있었습니다. 어떤 방식으로 CORS 동작을 확인하는지 정리합니다.
별 다른 설정없이 OPTIONS 으로 request를 날리면 ‘method is not allowed’ 응답을 만납니다.
Postman을 사용합니다. (편하신 다른툴 을 사용하셔도 됩니다.)
method: OPTIONS
URL: 검증할 URL
header 추가
Origin: 클라이언트 주소 또는 allow_origins 에 들어간 주소
Access-Control-Request-Headers: 본 요청 header
Access-Control-Request-Method: 본 요청 method
CORS 세팅이 정상이라면,
OK(plain text, http_status: 200) 응답을 받습니다.
CORS 세팅이 비정상이라면,
Disallowed CORS {origin | method | header} (plain text, http_status: 400) 응답을 받습니다.
제가 겪은 문제에서는 allow_origin에 * 가 아닌, “*” 를 넣어야 OK를 받는것을 보고 ‘”’ 를 strip하는 코드를 추가 했습니다.

공식문서에는 없는 문제

애플리케이션에서 사용할 Exception을 새로 정의합니다.
from http import HTTPStatus from core.enums import ResponseCode class CustomException(Exception): """Custom Base Exception""" http_code: int = HTTPStatus.INTERNAL_SERVER_ERROR message: str = "Server Runtime Exception"
Python
복사
에러를 핸들링 할 함수를 정의합니다.
app_ = FastAPI(...) @app_.exception_handler(CustomException) async def custom_exception_handler(request: Request, exc: CustomException): return JSONResponse( status_code=exc.http_code, content={"message": exc.message}, )
Python
복사
잘 동작할것 같지만 구멍이 있습니다.
1.
미들웨어, 앤드포인트 함수에서 raise 한 CustomException 은 클라이언트에서 CORS 에러가 발생합니다. (이는 python 기본 Exception인, ValueError, KeyError 등도 동일합니다.)
2.
starlette의 HttpException (=fastapi.exception.HTTPException)에 대해서는 어디서 raise 하던지 CORS 에러가 발생하지 않습니다.
3.
Depends() 에서 raise 한 CustomException 는 CORS 에러가 발생하지 않습니다.
차이는 리스폰스에서 Access-Control-Allow-Origin 의 유무입니다.
이에 대해 CORS middleware를 만든 Starlette의 입장은 문제 없다이며, FastAPI 에 등록된 이슈도 있으나 적용해도 해결이 되지 않아 해결책을 찾았습니다.
4071
issues
해결책: 응답 헤더에 {"Access-Control-Allow-Origin": "*"} 를 추가 함으로 이를 해결합니다. 저는 *를 추가했는데, allow_origin에 들어가는 클라이언트 주소를 넣으셔도 됩니다.
@app_.exception_handler(CustomException) async def custom_exception_handler(request: Request, exc: CustomException): return JSONResponse( headers={"Access-Control-Allow-Origin": "*"}, status_code=HTTPStatus.BAD_REQUEST, content={"code": exc.error_code, "message": exc.message}, )
Python
복사
Solution

공식문서에는 없는 문제2

서버 응답에서 Header 값을 브라우저에서 받아서 사용하기 위해서는 CORSMiddleware 의 expose_headers에 해당 Header 값이 추가되어야 합니다. 그렇지 않으면 브라우저에서 동작하는 javascript에서 Header에 접근이 안됩니다.
... app = FastAPI() origins = [ "*" ] expose_headers = [ "some-header", "some-header2", ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=expose_headers )
Python
복사
solution2

OO님 CORS 해결 했습니다

1.
프론트엔드 클라이언트에 대해서 유효한 Origin을 등록하고 적절한 Header와 method, expose_header를 설정 했습니다.
2.
서버 내부에러에 대해서도 브라우저에서 CORS 에러를 발생시키지 않습니다.
이 글이 FastAPI를 쓰면서 CORS로 받는 고통을 빠르게 덜어 드렸길!