- Published on
OpenAI Responses API 400 에러 원인 8가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 400을 돌려준다는 건 대개 요청 본문(JSON)이나 헤더가 스펙과 다르다는 뜻입니다. Responses API는 입력 형태가 유연한 대신, 필드 조합이 조금만 어긋나도 즉시 400이 납니다. 이 글은 "왜 400이 났는지"를 로그 한 줄로 역추적할 수 있도록, 현장에서 자주 만나는 원인 8가지를 증상, 잘못된 예시, 해결법 중심으로 정리합니다.
팁: 400을 재현할 때는 먼저
curl로 최소 요청을 만든 뒤, 필드를 하나씩 추가하세요. 대형 SDK 코드에서 바로 고치려 하면 원인이 섞여 진단이 늦어집니다.
1) Content-Type 누락 또는 JSON 파싱 실패
증상
- 400과 함께
invalid_json류 메시지 - 프록시/게이트웨이를 통과하면서 본문이 변형됨
흔한 원인
Content-Type: application/json누락- trailing comma, 따옴표 누락 등 JSON 문법 오류
- 서버에서 문자열을 이중 인코딩해 실제로는 JSON 문자열이 들어감
잘못된 예시
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{"model":"gpt-4.1-mini", "input": "hi",}'
해결
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":"hi"}'
추가로, 서버 사이드에서 요청 바디를 로깅할 때는 파싱 전 원문(raw body) 과 파싱 후 구조화된 객체를 둘 다 남기면 이중 인코딩을 빠르게 잡을 수 있습니다.
2) model 값 오타 또는 계정에서 접근 불가한 모델
증상
- 400과 함께 모델 관련 오류(모델을 찾을 수 없음, 지원하지 않음)
흔한 원인
- 모델 ID 오타
- 문서/예제에 나온 모델이 현재 프로젝트에서 비활성화
- 지역/조직 정책으로 특정 모델 접근 제한
해결 체크
- 먼저 최소 요청으로 모델만 검증
- 사용 중인 조직/프로젝트의 모델 허용 목록 확인
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":"model check"}'
운영에서는 모델명을 환경변수로 분리하고, 배포 시점에 smoke test로 위 최소 요청을 때려 모델 접근성을 확인하는 패턴이 안전합니다.
3) input 타입/구조가 스펙과 다른 경우
증상
- 400과 함께
invalid_request_error또는input관련 메시지
흔한 원인
input에 객체를 넣었는데 스펙과 다른 형태- 멀티모달 입력을 구성하면서
type/필수 필드 누락
잘못된 예시
{
"model": "gpt-4.1-mini",
"input": { "text": "hello" }
}
해결
가장 안전한 형태는 문자열로 시작해 성공을 확인한 뒤, 필요할 때 배열/구조로 확장하는 것입니다.
{
"model": "gpt-4.1-mini",
"input": "hello"
}
멀티모달을 쓴다면 input을 배열로 두고 각 아이템에 type을 명시하는 식으로 구성합니다(정확한 필드명은 사용 중인 SDK/문서 버전에 맞추세요).
4) response_format 또는 JSON 출력 강제 설정의 스키마 불일치
증상
- 400과 함께
response_format관련 오류 - "JSON으로 답하라"를 강제하려다 실패
흔한 원인
response_format에 잘못된 값- JSON 스키마를 넣었는데 필드 타입/required 정의가 틀림
- 모델이 지원하지 않는 출력 포맷을 강제
해결
우선 텍스트 응답으로 정상 동작을 확인한 뒤, JSON 강제를 단계적으로 적용하세요.
{
"model": "gpt-4.1-mini",
"input": "Return a JSON object with keys: ok (boolean)"
}
그 다음에만 response_format을 붙여 비교 테스트를 합니다. 운영에서는 포맷 강제 실패 시 텍스트로 폴백하는 전략(예: 재시도 시 포맷 옵션 제거)이 장애 전파를 줄입니다.
5) max_output_tokens 등 파라미터 범위/타입 오류
증상
- 400과 함께 파라미터가 유효하지 않다는 메시지
흔한 원인
- 숫자 필드에 문자열이 들어감(예:
"max_output_tokens":"1024") - 음수/0 등 범위 밖
- 서로 충돌하는 옵션 조합(예: 특정 모드에서만 가능한 옵션을 같이 사용)
잘못된 예시
{
"model": "gpt-4.1-mini",
"input": "hi",
"max_output_tokens": "1024"
}
해결
{
"model": "gpt-4.1-mini",
"input": "hi",
"max_output_tokens": 1024
}
타입 오류는 TypeScript/JSON Schema 검증으로 사전에 차단하는 게 가장 좋습니다. API 호출 직전에 런타임 밸리데이션을 두면 400을 "외부 장애"가 아니라 "내부 버그"로 빠르게 분류할 수 있습니다.
6) 멀티모달 입력에서 이미지 URL/인코딩 형식 문제
증상
- 400과 함께 이미지 입력이 유효하지 않다는 메시지
흔한 원인
httpURL, 사설망 URL, 만료된 서명 URLdata:URL(base64) 포맷이 깨짐- 너무 큰 이미지/지원하지 않는 포맷
해결 가이드
- 이미지 URL은 공개적으로 접근 가능하고, 만료 시간이 충분해야 합니다.
- base64를 쓴다면
data:image/png;base64,같은 프리픽스 포함 여부를 스펙에 맞추세요. - 프록시를 거친다면 이미지 서버의 리다이렉트가 의도치 않게 바뀌지 않는지 확인하세요.
멀티모달은 네트워크/프록시 이슈와도 자주 엮입니다. 스트리밍 사용 중 끊김이나 게이트웨이 튜닝까지 같이 봐야 한다면 아래 글의 체크리스트가 도움이 됩니다.
7) 스트리밍 설정(stream)과 클라이언트 처리 불일치
증상
- 서버는 400 또는 클라이언트에서 "예상치 못한 응답" 처리
- 중간 프록시가 응답을 버퍼링/변형
흔한 원인
stream: true인데 클라이언트가 SSE 파서를 안 씀- 사내 API Gateway가 SSE를 지원하지 않거나,
Accept/Connection헤더를 변경 - 응답을 gzip/버퍼링하면서 이벤트 경계가 깨짐
해결
- 먼저
stream을 끄고 정상 응답을 확인 - 그 다음 SSE를 켜고, 클라이언트가 이벤트 단위로 읽는지 확인
# 비스트리밍으로 먼저 확인
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"}'
# 스트리밍은 클라이언트가 SSE 처리 가능할 때만
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","stream":true}'
운영에서 스트리밍을 붙일 때는 Nginx/Envoy의 버퍼링 옵션, idle timeout, HTTP2 설정이 함께 영향을 줍니다. 위 내부 링크 글의 항목대로 하나씩 끄고 켜며 비교하면 원인 분리가 빨라집니다.
8) 인증 헤더는 맞는데 "프로젝트/키" 스코프가 다른 경우
증상
- 겉보기엔 인증이 맞아 보이는데도 400 또는 유사한 요청 오류
- 특정 환경(스테이징만, 특정 워커만)에서만 재현
흔한 원인
- 서로 다른 프로젝트 키를 섞어 사용(로컬은 A 프로젝트, 서버는 B 프로젝트)
- 프록시가
Authorization헤더를 덮어씀 - 키 회전 과정에서 일부 인스턴스만 구키를 사용
해결
- 배포 단위(파드/인스턴스)별로 실제 사용 중인 키 식별자를 로깅(전체 키를 남기지 말고 prefix나 해시만)
- 프록시/미들웨어에서
Authorization헤더가 보존되는지 확인 - 키 회전 시 blue-green 방식으로 동시 교체
인증 계층에서 400이 반복되면 OAuth 플로우를 쓰는 서비스에서도 유사한 형태로 터집니다. 특히 PKCE를 쓰는 경우 code_verifier 불일치가 대표적인 400 원인이니, 아래 글의 점검 포인트를 함께 참고하면 좋습니다.
재현 가능한 디버깅 루틴(운영용)
400을 "감"으로 고치지 않기 위한 순서를 추천합니다.
- 최소 요청으로 성공 확인:
model+input문자열만 - 헤더 고정:
Content-Type,Authorization확인 - 옵션을 하나씩 추가:
max_output_tokens,response_format,stream순 - 멀티모달은 마지막에: 이미지 URL 하나만 추가해서 검증
- 프록시 경유/직접 호출 비교: 게이트웨이에서 바디 변형 여부 확인
아래는 Node.js에서 요청 바디를 안전하게 구성하는 예시입니다(문자열 조합 대신 객체로 만들고 JSON.stringify를 한 번만 수행).
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function run() {
const payload = {
model: "gpt-4.1-mini",
input: "Diagnose why a 400 might happen.",
max_output_tokens: 256,
};
// SDK 사용
const res = await client.responses.create(payload);
console.log(res.output_text);
}
run().catch((e) => {
// 운영에서는 e.message만 보지 말고, request id/원문 payload도 함께 남기세요.
console.error(e);
});
마무리: 400은 "서버 장애"가 아니라 "요청 계약" 문제다
Responses API의 400은 대부분 아래 두 범주로 수렴합니다.
- 스키마/타입/조합 오류:
input,response_format, 토큰/옵션 타입 - 환경/경로 문제: 프록시가 JSON을 변형, 스트리밍 미지원, 키/프로젝트 혼선
위 8가지를 체크리스트처럼 순서대로 제거하면, "어떤 필드가 400을 만들었는지"를 짧은 시간 안에 특정할 수 있습니다. 특히 스트리밍이나 멀티모달을 붙이는 순간부터는 네트워크 계층까지 함께 보게 되므로, 반드시 비스트리밍 최소 요청을 기준선으로 잡고 비교 테스트를 진행하세요.