Frontend에서 FastAPI로 작성한 API의 엔드포인트 끝에 / 을 붙여서 요청하여 307 Temporary Redirect가 발생하고, 이로 인해 Mixed content 에러가 발생하는 실수를 하였습니다.
단순히 Frontend 코드에서 url 끝에 / 제거하여 해결하였지만, 이 현상을 더 자세히 살펴보고 블로그 글로 작성해보았습니다.
위 상황을 RESTfulAPI 관점에서 Trailing Slash 라고 한다고 합니다.
FastAPI가 Trailing Slash를 마주한 상황에서 내부적으로 어떻게 동작하는지 자세히 살펴보도록 하겠습니다.
FastAPI에서 아래 두 가지 URL 요청 방식은 언뜻 보기에 비슷해 보이지만, 실제 처리 과정에서 서로 다른 엔드포인트로 인식합니다.
GET /user/check?email={user_email}
GET /user/check/?email={user_email}
두 요청 모두 정상적인 요청이지만, FastAPI에서는 URL의 끝에 슬래시(/
)를 붙이는 경우와 붙이지 않는 경우를 내부적으로 다른 route로 간주합니다.
/check
와 /check/
는 다른 엔드포인트로 인식됩니다./check
로 라우트가 정의되어 있으면 /check/
요청 시 FastAPI는 307 Temporary Redirect
를 반환해 브라우저를 /check
로 리다이렉트합니다./check/
로 정의되었다면 /check
요청 시 자동으로 /check/
로 리다이렉트(307)합니다.이러한 동작으로 인해 의도하지 않은 307 Redirect가 발생할 가능성이 있습니다.
이전에 작성했던 FastAPI 구조 살펴보기 에서 말씀드렸다시피, FastAPI는 Starlette 프레임워크를 기반으로 구축되었습니다. 실제로 URL 끝의 슬래시 여부에 따른 리다이렉트 처리는 Starlette의 라우팅 시스템에서 이루어집니다. 아래는 Starlette의 리다이렉트 처리 코드입니다.
# Starlette의 routing.py 파일에서 발췌
from starlette.responses import RedirectResponse
class Router:
# ...
async def app(self, scope: Scope, receive: Receive, send: Send) -> None:
# ...
route_path = get_route_path(scope)
if scope["type"] == "http" and self.redirect_slashes and route_path != "/":
redirect_scope = dict(scope)
if route_path.endswith("/"):
redirect_scope["path"] = redirect_scope["path"].rstrip("/")
else:
redirect_scope["path"] = redirect_scope["path"] + "/"
for route in self.routes:
match, child_scope = route.matches(redirect_scope)
if match != Match.NONE:
redirect_url = URL(scope=redirect_scope)
response = RedirectResponse(url=str(redirect_url))
await response(scope, receive, send)
return
# ...
# ...
이 코드는 Starlette라는 Python 웹 프레임워크의 라우터(Router)에서 제공하는 기능 중 하나로, 사용자가 URL 경로 끝에 슬래시(/
)를 붙이거나 빼고 접속했을 때 적절한 주소로 리디렉션해주는 기능입니다.
예를 들어
/user
주소를 접속했는데, 실제 등록된 경로가 /user/
라면, 이 코드는 자동으로 /user/
로 리디렉션을 해줍니다./user/
주소를 접속했는데, 실제 등록된 경로가 /user
라면 /user
로 리디렉션을 해줍니다.이 기능은 URL의 일관성을 유지하고, 동일한 페이지가 여러 URL로 접근되는 것을 방지하는 데 도움을 줍니다.
if scope["type"] == "http" and self.redirect_slashes and route_path != "/":
http
인지 확인합니다. (웹 브라우저를 통해 들어온 일반 웹 요청)redirect_slashes=True
) 되어 있는지 확인합니다.(기본값이 true)route_path
)가 루트 경로(/
)가 아닌 경우에만 진행합니다. (루트 경로는 /
하나만 있는 것이기 때문에 리디렉션이 불필요함)redirect_scope = dict(scope)
if route_path.endswith("/"):
redirect_scope["path"] = redirect_scope["path"].rstrip("/")
else:
redirect_scope["path"] = redirect_scope["path"] + "/"
scope
) 정보를 복사하여 새로 리디렉션할 주소를 만듭니다./
)로 끝난다면, 슬래시를 제거한 주소로 바꿉니다. 예: /user/
→ /user
/user
→ /user/
for route in self.routes:
match, child_scope = route.matches(redirect_scope)
if match != Match.NONE:
redirect_scope
)가 현재 라우터에 등록된 경로인지 확인합니다.route.matches()
는 해당 경로와 현재 요청 주소가 일치하는지 체크하는 메서드입니다.redirect_url = URL(scope=redirect_scope)
response = RedirectResponse(url=str(redirect_url))
await response(scope, receive, send)
return
URL(scope=redirect_scope)
를 통해 구성합니다.RedirectResponse
클래스를 통해 브라우저에게 리디렉션 명령을 보낼 준비를 합니다. (기본적으로 HTTP 307 또는 308 리디렉션 코드 사용)response
)을 브라우저에 보내서 사용자가 브라우저에서 올바른 주소로 다시 접속하게 만듭니다.예시 상황 1: /user
로 접근했지만, 실제 URL은 /user/
인 경우
/user
로 요청 → 리디렉션 URL을 /user/
로 생성 → 이 경로가 실제 등록된 URL인지 확인 → 일치한다면 리디렉션 응답 발송 → 사용자는 /user/
로 다시 접속됨.예시 상황 2: /user/
로 접근했지만, 실제 URL은 /user
인 경우
/user/
로 요청 → 리디렉션 URL을 /user
로 생성 → 이 경로가 실제 등록된 URL인지 확인 → 일치한다면 리디렉션 응답 발송 → 사용자는 /user
로 다시 접속됨.즉, URL의 슬래시(/
) 유무에 따라 사용자를 올바른 주소로 안내하며, 이 과정에서 리다이렉트가 발생합니다.
아래처럼 Route를 작성한 상황을 가정해봅시다.
# routes/user.py
@app.get("/check")
def get_check_email(email: str):
# 함수 내용
이 경우 /check/
와 같이 뒤에 슬래시가 추가된 URL로 요청을 보내면 자동으로 307 Redirect가 발생합니다. 실제 FastAPI에서는 아래와 같은 과정이 일어납니다.
/user/check/?email={user_email}
요청/check/
가 아닌 /check
만 등록되어 있음을 확인/user/check?email={user_email}
으로 307 Temporary Redirect 응답 생성로컬에서 HTTP로 테스트할 때는 리다이렉트가 발생해도 문제가 없었습니다. 그 이유는 아래와 같습니다.
예를 들어, 로컬에서 다음과 같은 요청이 발생했을 때
GET http://localhost:8000/user/check/?email={user_email}
리다이렉트 후
GET http://localhost:8000/user/check?email={user_email}
두 URL 모두 HTTP 프로토콜을 사용하므로 문제가 없습니다.
하지만 실제 서버에 배포하고 HTTPS를 사용하게 되면 상황이 달라집니다.
예를 들어, 배포 환경에서 다음과 같은 요청이 발생했을 때:
GET https://example.com/user/check/?email={user_email}
리다이렉트 후 프로토콜이 변경
GET http://example.com/user/check?email={user_email}
이 경우 Mixed content 문제가 발생합니다. 현대 브라우저는 HTTPS 페이지에서 HTTP 리소스를 로드하는 것을 차단하므로, 리다이렉트 후 요청이 실패하게 됩니다.
요청 URL 끝에 슬래시(/
)가 추가된 경우, FastAPI는 기본적으로 307 Temporary Redirect
를 발생시켜 정의된 라우트로 자동으로 리다이렉트합니다. 이를 방지하기 위한 방법은 다음과 같습니다.
GET /user/check?email={user_email}
@router.get("/check", include_in_schema=True)
@router.get("/check/", include_in_schema=False)
def get_check_email(email: str):
# 함수 내용
redirect_slashes
매개변수를 False
로 설정from fastapi import FastAPI
# 슬래시 리다이렉트 비활성화
app = FastAPI(redirect_slashes=False)
이 방법을 사용하면 슬래시 차이에 따른 리다이렉트가 발생하지 않지만, 정의되지 않은 경로에 대해서는 404 에러가 발생합니다.