Django Ninja에서 Put/Patch 메서드로 파일을 다룰 때 생기는 422에러 문제 해결

개발자 강세영·2022년 9월 1일
0

TIL

목록 보기
45/70
post-thumbnail

Django ninja를 활용하여 프로젝트 개발을 잘 하고 있었는데 게시글 수정을 위한 API를 코딩하던 중 문제가 발새했다.
patch 메서드를 써서 게시글을 수정하는 API였는데 회원정보를 수정하는 API는 이 방법으로 아무 문제 없이 잘 되었다.

@router.patch("/{user_id}", response={200: SuccessOut, 404: NotFoundOut}, auth=[AuthBearer()])
def modify_user_info(request, user_id: int, payload: ModifyUserIn):

그래서 비슷하게 만들면 똑같이 잘 되겠지 하고 리퀘스트 바디에 업로드 파일을 받을수있도록 추가해서 만들었는데 아무리 테스트 해봐도 계속 422 에러가 떴다.

@router.patch("/{post_id}", response={200: SuccessOut, 400: BadRequestOut}, auth=AuthBearer())
def modify_post(request, post_id: int, body: ModifyPostIn = Form(...), file: UploadedFile = None):

아무리 제대로 된 값을 입력해도 리퀘스트 바디 스키마(ModifyPostIn)의 유효성 검사를 통과하지 못해 응답코드 422, 밸류 에러 리스폰스가 나왔다.

class ModifyPostIn(Schema):
    subject: Optional[str]
    content: Optional[str]

구글링해보니 ninja 깃헙 저장소의 이슈 포스팅으로 이미 문제가 제기된 부분이지만 이후에 패치가 되지 않았다. 이건 ninja보다는 Django 문제라서 그런것 같다.

문제 원인:

  • 문제는 ninja를 통해 PUT 또는 PATCH 메서드로 파일을 다루는 작업을 하면 Django에서 이를 제대로 처리하지 못한다는 것이다. POST를 쓰면 문제가 없었다.
  • 실제로 밑에서 미들웨어 추가를 하기 전에 메서드를 POST로 바꾸면 에러가 나지 않았다. 또한 업로드 파일이 없으면 multipart/form-data가 아니라 form-urlencoded를 쓰기 때문에 회원 정보를 수정하는 api는 메서드가 patch지만 아무 문제없이 잘 작동했다.
  • Form과 UploadedFile을 전부 사용하려면 리퀘스트 바디 content-type으로 multipart/form-data를 쓰게 된다.
  • 글을 수정하는 데이터는 application/json이나 form-urlencoded으로 처리되는데 이게 multipart/form-data로 통합되지 않아서 생기는 문제인 것 같다.
  • 파일만 form-data로 전송됐기 때문에 ModifyPostIn 스키마의 유효성 검사에서 글을 수정하는 데이터가 없다고 인식해서 422에러를 내는 것으로 생각된다.

해결방법:

  • PATCH 메서드를 쓸 때는 request.POST에 데이터가 없어야 된다고 한다. ninja 개발자가 직접 달아준 답변이다.
  • 리퀘스트 메서드를 임시로 POST로 바꾸고 request._load_post_and_files()로 form-data에 게시글 수정 데이터와 파일을 채워놓고 다시 원래 메서드(PUT 또는 PATCH)로 돌려놓는 미들웨어를 추가해서 해결했다.
  • Django HttpRequest의 _load_post_and_files() 메서드 소스코드 바로가기

미들웨어 코드

문제는 django 미들웨어 관리를 해본 적이 없어서 어떻게 해야 링크에 나온 코드를 미들웨어로 만들 수 있는지 몰랐다. 그래서 또 구글링을 해보니 비슷한 케이스가 있었다

MiddlewareMixin 클래스를 상속받아서 코드를 집어넣고 정해진 형식대로 미들웨어를 추가해주면 되는 것이었다. 덕분에 MiddlewareMixin이라는 클래스가 있다는 것과 활용방법을 배웠다.

  • MiddlewareMixin은 Django 1.1 이전 버전과 호환되는 미들웨어를 쉽게 만들기 위해서 지원하는 클래스인 것 같다.
# cores/middleware.py
from django.utils.deprecation import MiddlewareMixin

class PutPatchWithFileFormMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if request.method in ("PUT", "PATCH") and request.content_type != "application/json":
            if hasattr(request, '_post'): # post 데이터 삭제
                del request._post
                del request._files
            try:
                initial_method = request.method 
                request.method = "POST" # 리퀘스트 메서드를 POST로 임시 변경
                request.META['REQUEST_METHOD'] = 'POST'
                request._load_post_and_files() # 이 부분이 핵심
                request.META['REQUEST_METHOD'] = initial_method # 원래 메서드로 되돌림
                request.method = initial_method # 원래 메서드로 되돌림
            except Exception:
                pass
# settings.py 
# MIDDLEWARE 리스트에 다음 값 추가
# "폴더명.파일명.클래스명"
"cores.middleware.PutPatchWithFileFormMiddleware"

미들웨어가 꼭 클래스여야 할 필요는 없고 함수로도 가능하다.
단 팩토리 메서드 패턴이 적용된 함수여야 한다.

참고자료: https://inspireworld.tistory.com/m/106

0개의 댓글