Published on

OpenAI Responses API 400 에러 원인 8가지

Authors

서버가 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과 함께 이미지 입력이 유효하지 않다는 메시지

흔한 원인

  • http URL, 사설망 URL, 만료된 서명 URL
  • data: 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을 "감"으로 고치지 않기 위한 순서를 추천합니다.

  1. 최소 요청으로 성공 확인: model + input 문자열만
  2. 헤더 고정: Content-Type, Authorization 확인
  3. 옵션을 하나씩 추가: max_output_tokens, response_format, stream
  4. 멀티모달은 마지막에: 이미지 URL 하나만 추가해서 검증
  5. 프록시 경유/직접 호출 비교: 게이트웨이에서 바디 변형 여부 확인

아래는 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을 만들었는지"를 짧은 시간 안에 특정할 수 있습니다. 특히 스트리밍이나 멀티모달을 붙이는 순간부터는 네트워크 계층까지 함께 보게 되므로, 반드시 비스트리밍 최소 요청을 기준선으로 잡고 비교 테스트를 진행하세요.