본문 바로가기

Framework/FastAPI

[Fast API] Endpoint Validation (422 Error: Unprocessable Entity)

Fast API 애플리케이션 개발 시에 422 Error가 발생할 경우가 있다.

보통 API 요청 시 pydantic으로 정의되어 있는 Request Model에 어긋나기 때문에 발생하는데 그 외에도 endpoint, router 선언 기준/선언 순서에 따라 endpoint가 겹치는 현상이 발생하여 오류 추적이 어려울 때가 존재하여 그 내용을 정리하였다.


Endpoint 정의 및 순서에 따른 이슈

: 서로 다른 두 개의 API를 선언하고 테스트 결과를 확인해 보았다.

 

from fastapi import Body, FastAPI, Path
from model import UserInfo, UserStatus

app = FastAPI()

# 사용자 정보 수정 API
@app.put("/user/{id}")
def update_user(id: int = Path(...),request: UserInfo = Body(...),):
    user = update_user_info(id, request)

    return user

# 사용자 상태 수정 API
@app.put("/user/status")
def update_status(request: UserStatus):
    user = update_user_status(request)

    return user
    
def update_user_info(id, request):
    # DB Connection하여 user정보를 수정하는 함수로 가정함.
    return id

def update_user_status(request):
    # DB Connection하여 user의 status를 수정하는 함수로 가정함.
    return request

- 비교적 간단한 API 두 개를 선언하였으며, update_user_info, status 등 사용자 정보를 가져오거나 수정하는 부분은 구현하지 않았으나 구현되어 있는 것으로 가정하고 진행하였다.

 

- 사용자 정보 수정 API는 정상적으로 동작을 하였다. 문제가 된 부분은 사용자 상태 수정 API인데, 그 원인은 무엇일까?

 

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

class UserInfo(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None

class UserStatus(BaseModel):
    user_id: int
    status: str

class Board(BaseModel):
    board_id: int
    title: str
    content: str

- API의 model은 다음과 같이 선언되어 있으며, 아래의 swagger를 보면 알겠지만 model에 맞춰 정상적으로 호출한 것으로 보인다. 

 

- 실제 호출한 Curl을 봐도 정상적인 형태로 API를 호출한 것으로 확인이 된다. 

 

왜 실패했는가?

: 원인은 사용자 정보 수정 API에 숨어있다. 

 

- 두 API의 사양을 비교해 보자 

1) /user/{id}

2) /user/status

 

문제가 없는 것처럼 보일 수 있지만 두 API는 호출하는 endpoint가 겹치는 이슈가 존재한다.

두 API는 HTTP Method가 동일하며 endpoint 또한 "/user/status"를 호출한다면 1번 함수를 호출하고 {id} 파라미터에 status를 입력한 것으로 인지하게 된 것이다.

 

어떻게 해결해야 하는가?

: API의 호출순서를 변경하자

 

from fastapi import Body, FastAPI, Path
from model import UserInfo, UserStatus

app = FastAPI()

# 사용자 상태 수정 API
@app.put("/user/status")
def update_status(request: UserStatus):
    user = update_user_status(request)

    return user

# 사용자 정보 수정 API
@app.put("/user/{id}")
def update_user(id: int = Path(...),request: UserInfo = Body(...),):
    user = update_user_info(id, request)

    return user
    
def update_user_info(id, request):
    # DB Connection하여 user정보를 수정하는 함수로 가정함.
    return id

def update_user_status(request):
    # DB Connection하여 user의 status를 수정하는 함수로 가정함.
    return request

- API의 호출순서를 변경하고 테스트해 보자

 

- 호출이 정상적으로 성공한 것으로 확인된다.

 

이 외에도 router로 선언하는 방법이 존재한다. router로 endpoint를 나눠서 선언하고 main.py에서 router를 호출하는 순서로 조정이 가능하다.

 

하지만, 이를 방지하기 위해서는 초기 API 설계 시 endpoint를 확실하게 나누고 겹치지 않게 하는 것이 더 중요할 것으로 생각된다. 

위와 같은 이슈가 발생한다면 애초에 API를 다른 것을 호출하기 때문에 디버깅을 하기 어렵고 추측을 하여 찾아내야 되기 때문에 비교적 시간이 오래 걸릴 수 있다.

 

결론: API 설계 단계에 또는 신규 기능추가 시 API들끼리의 충돌이 발생하지 않게 주의가 필요하다.