파이썬 requests JSONDecodeError 해결: 빈 응답·HTML·상태 코드 확인 순서

response.json()을 바로 재호출하지 말고 상태 코드와 본문 존재 여부를 확인한 뒤 HTTP 오류를 분리하고 JSON 파싱을 시도한다. response.json()을 바로 재호출하지 말고 status_code와 본문 존재 여부를 확인한 뒤 HTTP 오류를 분리하고 JSON 파싱을 시도합니다. 빈 응답과 HTML 또는 잘못된 JSON은 모두 파싱 실패처럼 보일 수 있으므로 상태 코드·Content-Type·본문을 함께 확인해야 합니다. response.json()을 바로 호출하지 않고 상태 코드·본문·Content-Type을 확인하는 순서를 제시합니다. 빈 응답인지 HTTP 오류인지 HTML 응답인지 구분하면 재시도·기록·호출 수정 중 무엇을 할지 결정할 수 있습니다. 파싱 전에 실패 원인을 분리하면 자동화가 같은 잘못된 응답을 반복 처리하지 않게 할 수 있습니다. 다음 단계로는 JSON 파싱을 통과한 뒤 필수 키와 값 타입을 검증해야 합니다.

`response.json()`에서 JSONDecodeError가 나면 JSON 파싱을 반복하기 전에 응답의 상태 코드와 본문부터 확인해야 합니다. 빈 응답인지, HTTP 오류인지, HTML이나 잘못된 JSON이 도착했는지를 나누면 다음 행동을 정할 수 있습니다. 204 빈 응답에서는 response.json() 파싱을 시도하지 않습니다.

먼저 결론: json()보다 응답 상태를 먼저 봅니다

가장 안전한 순서는 `status_code`와 본문 존재 여부를 확인하고, HTTP 오류를 `raise_for_status()`로 분리한 다음, JSON이 필요한 응답에서만 `response.json()`을 호출하는 것입니다. JSON 파싱이 성공해도 HTTP 요청이 성공했다는 뜻은 아니므로 두 판단을 섞으면 안 됩니다.

1. 빈 응답인지 확인합니다

HTTP 204는 `NO_CONTENT`입니다. 본문이 없는 응답에 JSON이 들어 있다고 가정하면 `response.json()`에서 예외가 날 수 있습니다. 삭제나 상태 변경처럼 성공했지만 반환할 데이터가 없는 API라면 빈 응답을 별도 성공 경로로 처리해야 합니다.

import requests

response = requests.get("https://api.example.com/item")

if response.status_code == requests.codes.no_content:
    result = None
else:
    response.raise_for_status()
    result = response.json()

HTTP 오류를 JSON 파싱 오류와 분리합니다

서버가 오류 상태를 반환하면서 JSON 오류 설명을 줄 수도 있고, HTML 오류 페이지를 줄 수도 있습니다. 이때 먼저 `raise_for_status()`를 호출하면 HTTP 오류를 JSONDecodeError와 다른 문제로 기록할 수 있습니다.

response.raise_for_status()
data = response.json()

반대로 파싱 성공만 보고 정상 처리하면 안 됩니다. 실패 상태의 응답에도 JSON 객체가 담길 수 있기 때문입니다.

본문과 Content-Type을 제한적으로 확인합니다

상태 코드가 기대한 값인데도 파싱이 실패하면 `headers.get(“Content-Type”)`과 본문 앞부분을 확인합니다. HTML이 들어왔다면 로그인 페이지, 프록시 오류 화면, 서버 오류 화면 등 JSON이 아닌 응답일 가능성을 의심할 수 있습니다. 다만 본문에는 토큰이나 개인정보가 섞일 수 있으니 전체를 로그로 남기지 말고, 호출 쪽 로그 정책에서 정한 길이를 함수에 전달합니다.

def inspect_response(response, preview_limit):
    content_type = response.headers.get("Content-Type", "")
    preview = response.text[:preview_limit]

    if "json" not in content_type.lower():
        raise ValueError(f"JSON이 아닌 응답: {content_type!r}, preview={preview!r}")

    return response.json()

`Content-Type`이 맞아도 실제 본문이 유효하다는 보장은 없으므로 마지막에는 JSON 파싱 예외를 처리해야 합니다. HTML 여부를 문자열만으로 단정하기보다 상태 코드·헤더·본문을 함께 봐야 합니다.

운영에서 사용할 최소 진단 함수

import requests


def read_json_response(response: requests.Response, preview_limit: int):
    if response.status_code == requests.codes.no_content:
        return None

    response.raise_for_status()

    content_type = response.headers.get("Content-Type", "")
    if "json" not in content_type.lower():
        raise ValueError(f"expected JSON, got {content_type!r}")

    try:
        return response.json()
    except requests.exceptions.JSONDecodeError as exc:
        preview = response.text[:preview_limit]
        raise ValueError(f"invalid JSON response: {preview!r}") from exc

이 함수는 타임아웃, 재시도, JSON 내부 필수 키 검증까지 해결하지 않습니다. 현재 문제는 응답 형식을 판별하는 단계이므로, 이 함수가 반환한 값에 대해 다음 단계에서 필요한 키와 자료형을 별도로 확인해야 합니다.

정리

`response.json()`에서 JSONDecodeError가 나면 먼저 204인지 확인하고, 그다음 `raise_for_status()`로 HTTP 오류를 분리합니다. 정상 상태를 기대했는데도 실패하면 Content-Type과 제한된 본문 미리보기를 확인한 뒤 JSON 파싱 예외를 기록합니다. 이 순서를 지키면 빈 응답·HTML 오류 페이지·잘못된 JSON을 같은 원인으로 오해하지 않고 중단, 재처리, 호출 수정 중 하나를 선택할 수 있습니다.

다음 질문은 JSON 파싱을 통과한 값이 자동화가 기대하는 필수 키와 자료형을 갖췄는지 확인하는 방법입니다.

출처: Requests 공식 Quickstart, Requests 공식 API 문서, Python `http.HTTPStatus` 공식 문서

Leave a Comment