- Published on
OpenAI Responses API 415 Unsupported Media Type 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 415 Unsupported Media Type를 반환한다는 건, 요청 본문(body)의 미디어 타입을 서버가 이해하지 못하거나(Content-Type 불일치), 아예 본문 파싱 단계에서 실패했다는 의미입니다. OpenAI Responses API에서 이 에러는 대부분 “JSON으로 보내야 하는데 JSON이 아니게 전송됨”, “멀티파트를 잘못 구성함”, “프록시/게이트웨이가 Content-Type을 바꿔치기함” 같은 전송 계층의 실수로 발생합니다.
이 글에서는 415를 재현 → 원인 분류 → 언어별 수정 코드 → 운영 환경에서의 방어 로직 순서로 정리합니다.
415가 뜨는 전형적인 상황 5가지
1) Content-Type: application/json이 빠졌거나 잘못됨
Responses API의 기본 요청은 JSON입니다. 그런데 다음 케이스가 흔합니다.
Content-Type을 아예 안 넣음text/plain,application/x-www-form-urlencoded로 들어감multipart/form-data를 수동으로 넣고 boundary를 누락함
특히 curl에서 -d만 쓰면 기본이 application/x-www-form-urlencoded로 잡히는 환경(또는 래퍼 스크립트)에서 415가 잘 터집니다.
2) JSON을 보내는 척하지만 실제로는 “문자열”을 전송
예: Python requests.post(..., data=json.dumps(payload))를 쓰면서 헤더를 빼먹거나, Node에서 body: payload(객체)인데 JSON.stringify를 안 하는 경우.
서버 입장에서는 Content-Type은 JSON인데 body가 JSON 파서로 해석 불가라서 400/415로 떨어질 수 있습니다(게이트웨이/프록시 구성에 따라 415로 매핑되기도 함).
3) 파일/이미지를 JSON 본문에 그대로 넣음(잘못된 업로드 방식)
Responses API에서 파일/이미지 입력은 보통 파일 업로드 엔드포인트를 따로 쓰거나, 지정된 포맷(예: URL, file_id, 혹은 지원되는 입력 구조)을 사용해야 합니다.
바이너리 데이터를 JSON에 그대로 박아 넣거나, multipart/form-data로 보내면서 필드 구성이 잘못되면 415가 발생합니다.
용량이 커서 실패하는 경우는 413으로 떨어지는 편이므로, 415면 우선 “형식”을 의심하세요. 큰 업로드 자체의 전략은 아래 글도 같이 참고하면 좋습니다.
4) 프록시/게이트웨이가 헤더를 변조하거나 제거
Nginx, API Gateway, Cloudflare, 사내 프록시가 다음을 수행하면 415가 생깁니다.
Content-Type을 강제로 덮어씀Transfer-Encoding: chunked처리 과정에서 body를 재인코딩Expect: 100-continue처리 버그
이 경우 클라이언트 코드는 정상인데도 서버가 415를 주므로, “실제 wire-level 요청”을 캡처해야 합니다.
5) 스트리밍/비동기 구현에서 body가 깨짐
스트리밍 응답을 받는 코드가 아니라, 요청을 보내는 쪽에서 chunk를 잘못 쓰거나(특히 저수준 http 라이브러리) Content-Length가 틀어지면 서버가 body를 파싱하지 못해 415/400을 반환할 수 있습니다.
스트리밍 자체의 타임아웃/끊김 이슈는 별개로 빈번하니, 운영에서는 함께 점검하는 편이 좋습니다.
가장 먼저 확인할 “정답 요청 형태”
Responses API의 핵심은 JSON POST입니다. 아래는 최소 형태의 예시입니다(필드명은 사용 중인 모델/기능에 따라 달라질 수 있으나, 핵심은 “JSON + 올바른 Content-Type”입니다).
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4.1-mini",
"input": "Say hello in Korean"
}'
여기서 415가 난다면, 높은 확률로 (1) 헤더가 실제로 전달되지 않았거나 (2) 프록시가 바꿔치기했거나 (3) 바디가 JSON이 아닌 형태로 전송되고 있습니다.
언어별 415 재현 코드와 수정 코드
Python (requests) — data= 대신 json=을 쓰는 게 안전
잘못된 예(415/400 유발 가능)
import requests, json
payload = {"model": "gpt-4.1-mini", "input": "hello"}
# Content-Type을 빼먹거나, data=로 보내면 환경에 따라 폼 인코딩/문자열 처리로 꼬일 수 있음
r = requests.post(
"https://api.openai.com/v1/responses",
headers={"Authorization": f"Bearer {OPENAI_API_KEY}"},
data=json.dumps(payload),
)
print(r.status_code, r.text)
올바른 예
import requests
payload = {"model": "gpt-4.1-mini", "input": "hello"}
r = requests.post(
"https://api.openai.com/v1/responses",
headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json",
},
json=payload, # requests가 JSON 직렬화 + 헤더를 일관되게 처리
timeout=30,
)
print(r.status_code)
print(r.json())
json=을 쓰면 직렬화/헤더 설정 실수를 줄일 수 있습니다. 다만 사내 프록시가 헤더를 건드리는 환경이라면, 아래 “와이어 레벨 확인”까지 진행해야 합니다.
Python (httpx) — content=와 json= 혼동 금지
import httpx
payload = {"model": "gpt-4.1-mini", "input": "hello"}
with httpx.Client(timeout=30) as client:
r = client.post(
"https://api.openai.com/v1/responses",
headers={"Authorization": f"Bearer {OPENAI_API_KEY}"},
json=payload,
)
r.raise_for_status()
print(r.json())
content=는 바이트/문자열 raw body를 직접 넣을 때 사용합니다.json=은 올바른 JSON 직렬화 +Content-Type: application/json을 보장합니다.
네트워크 오류/타임아웃 재시도 설계는 415와 별개지만, 운영에서는 함께 넣는 경우가 많습니다.
Node.js (fetch) — JSON.stringify 누락이 가장 흔한 실수
잘못된 예
await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: { model: "gpt-4.1-mini", input: "hello" }, // 객체 그대로 들어가면 깨짐
});
올바른 예
await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4.1-mini",
input: "hello",
}),
});
curl — -F(multipart)와 -d(json)를 섞지 말 것
- JSON 요청:
-d+Content-Type: application/json - 멀티파트 요청:
-F를 쓰되 Content-Type을 수동으로 박지 말고 curl이 boundary를 만들게 두는 편이 안전
잘못된 예(멀티파트를 수동으로 선언)
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: multipart/form-data" \
-F "model=gpt-4.1-mini" \
-F "input=hello"
위처럼 multipart/form-data를 수동으로 넣으면 boundary가 없어서 서버가 해석 못 할 수 있습니다.
수정 예
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F "model=gpt-4.1-mini" \
-F "input=hello"
다만 Responses API는 기본적으로 JSON을 권장/가정하는 경우가 많으니, 파일 업로드가 필요하지 않다면 멀티파트 자체를 피하고 JSON으로 고정하는 것이 415 예방에 가장 좋습니다.
415를 10분 안에 끝내는 디버깅 체크리스트
###[1] 서버에 실제로 전달된 Content-Type을 확인 클라이언트 코드에서 헤더를 설정했더라도, 중간 프록시가 바꿀 수 있습니다.
- 로컬:
mitmproxy,tcpdump,wireshark - 서버 앞단: Nginx access log에
$http_content_type기록
Nginx 예:
log_format with_ct '$remote_addr - $request $status ct=$http_content_type len=$content_length';
access_log /var/log/nginx/access.log with_ct;
###[2] body가 진짜 JSON인지(첫 바이트부터) 확인 서버가 기대하는 JSON은 보통 { 또는 [로 시작합니다.
body가model=gpt...&input=...형태면 폼 인코딩입니다.body가"{...}"처럼 JSON 문자열이 한 번 더 감싸진 형태면 이중 인코딩입니다.
###[3] charset는 대체로 문제 아님, boundary는 문제
application/json; charset=utf-8는 보통 OKmultipart/form-data는 boundary가 없으면 거의 즉시 실패
###[4] SDK/라이브러리 버전과 래퍼 레이어 점검 사내 공통 HTTP 클라이언트(인터셉터)가 다음을 하는지 확인하세요.
- 모든 POST를
application/x-www-form-urlencoded로 강제 - body를 URL 인코딩
Content-Type을 덮어쓰기
###[5] 415 다음에 422가 연쇄로 뜨면 “형식”은 해결, “스키마”가 문제 415를 해결하고 나면, 이제 서버가 JSON을 파싱할 수 있게 되어 **422(스키마 검증)**가 드러나는 경우가 많습니다. 그때는 입력 스키마/필드명을 점검해야 합니다.
운영 환경에서 415를 예방하는 패턴
1) Content-Type 강제 + JSON 직렬화 단일화
팀 내에서 “요청을 만드는 유일한 함수”를 두고, 항상 json=(Python) / JSON.stringify(Node)로만 보내게 하면 415가 급감합니다.
Python 예:
import httpx
def post_responses(payload: dict, *, api_key: str) -> dict:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
with httpx.Client(timeout=30) as client:
r = client.post("https://api.openai.com/v1/responses", headers=headers, json=payload)
r.raise_for_status()
return r.json()
2) 프록시를 쓴다면 “헤더 보존”을 계약으로 못 박기
Content-Type,Content-Length,Authorization은 변조 금지- 가능하면 TLS 종단을 애플리케이션 가까이 두기
3) 에러 로깅에 반드시 요청 메타데이터를 남기기(민감정보 제외)
Content-Type,Content-Length, endpoint path- body는 원문 전체 대신 처음 200바이트 정도만(PII/키 제외)
마무리
Responses API에서 415는 대부분 “모델/파라미터 문제”가 아니라 HTTP 요청 포맷 문제입니다. 해결 순서는 단순합니다.
Content-Type: application/json이 실제로 전달되는지 확인- body가 실제 JSON인지 확인(
json=/JSON.stringify로 통일) - 멀티파트를 쓸 땐 boundary를 자동 생성하게 두고, 가능하면 JSON으로 고정
- 프록시/게이트웨이가 헤더를 덮어쓰지 않는지 wire-level로 검증
이 과정을 거치면 415는 대부분 즉시 해결되고, 다음 단계로 422(스키마)나 401/403(인증) 같은 “진짜 문제”가 드러납니다.