Published on

OpenAI Responses API 400 image_parse_error 해결 가이드

Authors

서버에서 이미지 입력을 붙인 뒤 OpenAI Responses API를 호출했는데, 갑자기 400 Bad Request와 함께 image_parse_error가 떨어지면 당황스럽습니다. 이 에러는 “이미지로 보이는 값을 받긴 했지만, OpenAI 쪽 파서가 정상적인 이미지로 해석하지 못했다”는 의미에 가깝습니다. 즉 **네트워크/인증 문제가 아니라 입력 이미지 자체(바이트/포맷/인코딩/URL 접근성/요청 구조)**에 문제가 있을 확률이 큽니다.

이 글에서는 image_parse_error원인별로 빠르게 분류하고, 재현 → 검증 → 수정까지 이어지는 실전 해결 절차를 정리합니다. (용량 제한으로 터지는 413 계열은 별도 이슈이므로, 해당 가능성이 크면 먼저 OpenAI Responses API 413 에러 업로드 용량 제한과 청크 전략도 함께 확인하세요.)

1) image_parse_error가 의미하는 것 (관찰 포인트)

image_parse_error는 보통 아래 중 하나로 귀결됩니다.

  • data URL 형식이 잘못됨: data:image/png;base64,.... 형태가 아니거나, base64, 뒤가 깨짐
  • Base64 디코딩은 되지만 이미지 디코딩이 실패: 바이트가 손상되었거나, MIME 타입과 실제 포맷이 불일치
  • 이미지 파일이 “이미지처럼 보이지만” 실제론 HTML/JSON/텍스트: URL이 403/404/리다이렉트 페이지를 반환
  • 부분 업로드/부분 읽기: 스트리밍 중간에 끊기거나, 파일을 끝까지 읽기 전에 인코딩
  • 지원하지 않는 포맷/특이한 인코딩: 일부 TIFF/HEIC/CMYK JPEG, 손상된 ICC profile 등
  • 요청 스키마 오류로 이미지가 다른 필드로 들어감: input 구조가 잘못되어 이미지가 문자열로 처리

핵심은 “OpenAI가 받은 최종 바이트열이 정상 이미지로 파싱 가능했는가”입니다. 따라서 클라이언트에서 보낸 최종 페이로드를 그대로 재검증하는 것이 가장 빠릅니다.

2) 가장 흔한 원인 Top 7과 즉시 체크리스트

2.1 data URL 접두사 누락/오타

정상 예:

  • data:image/png;base64,<BASE64>
  • data:image/jpeg;base64,<BASE64>

자주 하는 실수:

  • data:img/png;base64,... (MIME 오타)
  • data:image/png;base64 <BASE64> (콤마 누락)
  • data:image/png; base64,... (세미콜론/공백 위치 이상)

2.2 MIME 타입과 실제 바이트 포맷 불일치

예를 들어, 실제 파일은 PNG인데 image/jpeg로 붙이거나 그 반대.

  • PNG 시그니처: 89 50 4E 47 0D 0A 1A 0A
  • JPEG 시그니처: FF D8 FF

MIME은 “설명”일 뿐이고, 파서는 실제 바이트를 보고 실패할 수 있습니다.

2.3 Base64가 URL-safe 변형(-, _)이거나 padding(=)이 제거됨

일부 시스템은 Base64를 URL-safe로 바꾸거나 = padding을 제거합니다.

  • 표준 Base64: + / = 사용
  • URL-safe Base64: - _ 사용

디코더가 표준을 기대하면 깨질 수 있습니다. 항상 표준 Base64로 인코딩하고, 중간에 URL 인코딩을 하지 마세요.

2.4 파일 읽기/전송이 “부분만” 됨 (truncate)

업로드 중간에 끊기거나, 비동기 처리에서 파일 핸들이 닫히기 전에 읽는 경우.

  • 로컬에서 이미지 뷰어로는 열리는데 API에서 실패한다면, 전송 과정에서 잘렸을 가능성이 큽니다.
  • 특히 S3 presigned URL을 받아서 서버가 다시 fetch하는 구조에서 흔합니다.

2.5 URL을 이미지로 넘겼는데 실제 응답이 이미지가 아님

이미지 URL을 전달했지만 서버가:

  • 302 로그인 페이지로 리다이렉트
  • 403 AccessDenied HTML
  • 404 Not Found HTML
  • CloudFront/WAF 차단 페이지

을 반환하면, OpenAI는 “이미지 바이트”가 아니라 “HTML”을 받아 파싱에 실패합니다.

2.6 색공간/포맷 이슈 (CMYK JPEG, HEIC 등)

일부 환경에서는 HEIC/HEIF, CMYK JPEG, 손상된 EXIF/ICC profile이 문제를 일으킵니다. 가장 안전한 우회는:

  • PNG 또는 표준 sRGB JPEG로 재인코딩
  • 이미지 리사이즈/재저장(메타데이터 제거)

2.7 요청 JSON 구조 문제로 이미지가 잘못 들어감

Responses API는 input 배열 안에 텍스트/이미지 콘텐츠를 넣는 구조를 사용합니다. 구조가 틀리면 이미지가 문자열로 들어가거나, type이 누락되어 파싱 단계에서 실패할 수 있습니다.

3) 재현 가능한 “로컬 검증 루틴” 만들기 (가장 확실한 해결책)

에러를 한 번에 잡으려면 API 호출 전에 다음을 검증하세요.

  1. Base64 디코딩이 되는가?
  2. 디코딩한 바이트가 실제 이미지로 열리는가?
  3. MIME 타입이 바이트 시그니처와 일치하는가?
  4. (URL 입력이라면) URL이 이미지 Content-Type을 반환하는가?

아래는 Python으로 “API에 보낼 data URL”을 사전 검증하는 코드입니다.

3.1 Python: data URL 검증 + 이미지 재인코딩(권장)

import base64
import re
from io import BytesIO
from PIL import Image

DATA_URL_RE = re.compile(r"^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$")


def validate_and_normalize_data_url(data_url: str, force_format: str = "PNG") -> str:
    """검증 후 안전한 포맷(PNG/JPEG)으로 재인코딩한 data URL을 반환."""
    m = DATA_URL_RE.match(data_url.strip())
    if not m:
        raise ValueError("Invalid data URL format. Expected data:image/<type>;base64,<...>")

    mime, b64 = m.group(1), m.group(2)

    # base64 디코딩 검증
    try:
        raw = base64.b64decode(b64, validate=True)
    except Exception as e:
        raise ValueError(f"Base64 decode failed: {e}")

    # 이미지 디코딩 검증
    try:
        img = Image.open(BytesIO(raw))
        img.load()  # 실제 디코딩 강제
    except Exception as e:
        raise ValueError(f"Image decode failed (corrupt/unsupported): {e}")

    # 안전하게 재인코딩(메타데이터/색공간 이슈 제거)
    out = BytesIO()
    if force_format.upper() in ("JPG", "JPEG"):
        img = img.convert("RGB")
        img.save(out, format="JPEG", quality=92, optimize=True)
        out_mime = "image/jpeg"
    else:
        # PNG는 알파/텍스트 등 다양한 경우에 안전
        img.save(out, format="PNG", optimize=True)
        out_mime = "image/png"

    out_b64 = base64.b64encode(out.getvalue()).decode("ascii")
    return f"data:{out_mime};base64,{out_b64}"

이 루틴을 거치면 다음 유형의 문제를 한 번에 제거합니다.

  • 깨진 Base64
  • 손상/미지원 이미지 포맷
  • CMYK/ICC profile/EXIF 등 메타데이터로 인한 디코딩 문제
  • MIME-실데이터 불일치(재인코딩으로 정규화)

4) Responses API에 올바르게 이미지 넣는 예시

아래는 input에 텍스트와 이미지를 함께 넣는 전형적인 형태입니다(이미지 data URL 사용).

from openai import OpenAI

client = OpenAI()

data_url = "data:image/png;base64,iVBORw0KGgoAAA..."  # 검증/정규화된 값 권장

resp = client.responses.create(
    model="gpt-4.1-mini",
    input=[
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": "이 이미지에서 텍스트를 읽고 요약해줘."},
                {"type": "input_image", "image_url": data_url},
            ],
        }
    ],
)

print(resp.output_text)

여기서 실무적으로 중요한 포인트:

  • content는 문자열이 아니라 배열이며, 각 원소에 type이 있어야 합니다.
  • input_image에서 image_url에는 (1) https URL 또는 (2) data URL이 들어갑니다.
  • data URL에는 반드시 data:image/...;base64, 접두사가 필요합니다.

5) URL 이미지를 쓰는 경우: “진짜 이미지”인지 먼저 확인

외부 URL을 넣을 때는 OpenAI가 해당 URL에 접근해 이미지를 가져옵니다. 따라서 여러분 서버에서는 정상으로 보이더라도, OpenAI 입장에서는 다음 문제가 생길 수 있습니다.

  • 사내망/VPC 내부 주소라 접근 불가
  • User-Agent/지역/WAF 정책으로 차단
  • 서명 URL 만료
  • 리다이렉트 체인(로그인/동의 페이지)

5.1 Python: URL이 이미지인지 사전 점검

import httpx


def assert_image_url(url: str) -> None:
    with httpx.Client(follow_redirects=True, timeout=10.0) as client:
        r = client.get(url, headers={"Accept": "image/*"})
        r.raise_for_status()
        ctype = r.headers.get("Content-Type", "")
        if not ctype.startswith("image/"):
            body_prefix = r.text[:200] if "text" in ctype or ctype == "" else "<non-text body>"
            raise ValueError(f"Not an image response. Content-Type={ctype}, body_prefix={body_prefix!r}")

        # 바이트 길이도 sanity check
        if len(r.content) < 64:
            raise ValueError("Response too small to be a valid image")

이 검증에서 실패하면 image_parse_error로 이어질 가능성이 매우 높습니다. URL 기반 입력이 불안정하다면, 서버에서 이미지를 다운로드 후 data URL로 변환해 보내는 방식이 디버깅과 안정성 측면에서 유리합니다.

6) 서버에서 자주 터지는 케이스별 처방전

6.1 프론트에서 넘어온 Base64에 공백/개행이 섞임

  • 이메일/폼/로그를 거치며 개행이 삽입되는 경우가 있습니다.
  • 해결: strip() 후, Base64 본문에서 공백/개행 제거(단, 원칙은 생성 단계에서 깨끗하게 만들기)
import re

b64_clean = re.sub(r"\s+", "", b64_string)

6.2 멀티파트 업로드 후 파일 포인터 위치 문제

예: FastAPI에서 UploadFile.file.read()를 한 번 호출하고 다시 읽으면 빈 바이트가 됩니다.

# 잘못된 예: 이미 한 번 읽어서 포인터가 EOF
raw1 = await upload.read()
raw2 = await upload.read()  # b""

# 올바른 예: 한 번만 읽어 저장
raw = await upload.read()

또는 파일 객체라면 seek(0)로 되돌리세요.

6.3 이미지 리사이즈/압축 파이프라인에서 손상

OpenCV/Pillow 처리 후 저장 포맷/색공간이 꼬일 수 있습니다. 항상 최종 산출물을 로컬에서 다시 열어 검증하세요.

from PIL import Image
from io import BytesIO

img = Image.open("input.jpg")
out = BytesIO()
img.convert("RGB").save(out, format="JPEG", quality=90)
# 검증
out.seek(0)
Image.open(out).load()

7) 관측/로깅: “무슨 바이트를 보냈는지” 남겨야 빨리 끝난다

image_parse_error는 입력 데이터 문제이므로, 다음을 로깅하면 원인 규명이 빨라집니다.

  • 이미지 소스 유형: url vs data_url
  • data URL이면:
    • MIME 문자열
    • base64 길이
    • 디코딩 후 바이트 길이
    • SHA256 해시(개인정보/원본 노출 없이 동일성 비교 가능)
  • URL이면:
    • 최종 응답 URL(리다이렉트 후)
    • 응답 status code
    • Content-Type, Content-Length

스트리밍/네트워크 이슈로 이미지 다운로드가 불완전해지는 경우도 있으니, 서버의 HTTP 클라이언트 재시도 설계가 필요할 수 있습니다. 이 경우에는 Python httpx ReadTimeout·ConnectError 재시도 설계와 함께, 스트리밍이 끊길 때의 복구 패턴은 OpenAI Responses API 스트리밍 끊김 타임아웃 완전 복구 가이드를 참고하면 좋습니다.

8) 최종 점검표 (운영에서 바로 쓰는 버전)

8.1 data URL을 보낼 때

  • data:image/<type>;base64, 접두사 정확
  • Base64는 표준 인코딩(+ / =)이며 URL 인코딩되지 않음
  • 디코딩 후 PIL.Image.open(...).load() 성공
  • 가능하면 PNG 또는 sRGB JPEG로 재인코딩
  • (대용량이면) 413 가능성 점검 및 축소/청크 전략 적용

8.2 URL을 보낼 때

  • OpenAI가 접근 가능한 공개 URL(사설망/VPN 주소 금지)
  • 만료되지 않는 URL(또는 충분히 긴 presigned 만료)
  • Content-Type: image/* 반환
  • 302/403/404 등으로 HTML 내려주지 않음
  • WAF/봇차단이 User-Agent에 따라 다르게 동작하지 않음

9) 결론: 해결의 핵심은 “전송 직전 바이트 검증 + 재인코딩”

400 image_parse_error는 대부분 (1) data URL 형식 오류, (2) 이미지 바이트 손상/불일치, (3) URL이 실제 이미지를 반환하지 않음 세 갈래로 정리됩니다. 가장 확실한 해결책은 API 호출 직전에 다음을 자동화하는 것입니다.

  • 입력이 URL이면: 서버에서 먼저 받아 Content-Type/바이트 길이/이미지 디코딩을 검증
  • 입력이 Base64면: 디코딩 및 Pillow로 로드 테스트
  • 운영 안정성을 위해: PNG 또는 표준 JPEG로 재인코딩하여 정규화

이 과정을 넣으면 image_parse_error는 “가끔 터지는 미스터리”가 아니라, 사전에 차단되는 입력 품질 문제로 바뀝니다.