- Published on
Gemini API 400 Invalid Argument - 안전필터·스키마 오류
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Gemini API를 붙이다 보면 인증(401/403)보다 더 당황스러운 에러가 400 Invalid Argument입니다. 이유는 단순합니다. 요청은 서버에 도달했는데, 모델이 처리할 수 없는 형태(스키마)거나 정책상 허용되지 않는 형태(안전 필터)로 해석되면 400으로 떨어지기 때문입니다. 특히 response_schema를 붙여 구조화 출력(JSON)까지 요구하거나, Safety 설정을 조정하는 순간 400의 빈도가 급격히 올라갑니다.
이 글은 “대충 파라미터 하나 잘못 넣었나?” 수준이 아니라, (1) 안전필터 관련 400과 (2) 스키마/요청 바디 스펙 관련 400을 분리해 빠르게 원인에 도달하는 방법을 정리합니다. 운영 환경에서 재현이 어려울 때를 대비해 로그 설계/검증 코드도 함께 제공합니다.
1) 400 Invalid Argument를 먼저 분류하라
Gemini의 400은 대략 아래 두 갈래로 나뉩니다.
- 안전필터/정책 관련: 프롬프트 또는 출력 요구가 정책에 걸리거나, Safety 설정 값이 허용되지 않는 조합일 때
- 스키마/요청 형식 관련:
contents,parts,tools,generationConfig,responseSchema등 요청 JSON의 구조/타입이 스펙과 다를 때
실무에서는 에러 메시지에 fieldViolations가 포함되는 경우가 많습니다. 이때 핵심은 “어떤 필드가 invalid인지”를 정확히 로그로 남기는 것입니다.
진단 체크리스트(5분 컷)
- HTTP Response body를 원문 그대로 저장(마스킹 후)
error.details[].fieldViolations[]유무 확인contents/parts구조가 맞는지 확인(문자열을 바로 넣는 실수 빈번)responseSchema가 JSON Schema 규격에 맞는지 확인(특히type,properties,required)- Safety 설정을 허용되는 enum 값/조합으로만 사용했는지 확인
운영 장애 대응 관점에서 비슷한 “원인은 단순한데 로그가 없어 헤매는” 유형은 다른 시스템에서도 반복됩니다. 예를 들어 LLM RAG에서 환각/반복을 디버깅할 때도 “재현 가능한 입력·출력 로그”가 성패를 가릅니다. 관련해서는 LangChain LlamaIndex RAG 디버깅 체크리스트도 함께 참고하면 좋습니다.
2) 안전필터(Safety) 때문에 400이 나는 패턴
Gemini는 “유해 콘텐츠를 생성하지 말라” 수준을 넘어, 요청 자체가 정책 위반이거나 Safety 설정이 비정상이면 400으로 거절할 수 있습니다. 아래는 자주 보는 패턴입니다.
2.1 Safety 설정 값/키가 스펙과 다름
SDK/버전에 따라 Safety 설정 필드명이 다르거나, 허용 enum이 바뀌기도 합니다. 예를 들어 아래처럼 존재하지 않는 카테고리/threshold를 넣으면 400이 날 수 있습니다.
{
"safetySettings": [
{
"category": "HARM_CATEGORY_SOMETHING_NEW",
"threshold": "BLOCK_NONE"
}
]
}
대응
- 사용 중인 SDK 버전(또는 REST 스펙)에서 지원하는
category,thresholdenum을 확인 - 한 번에 여러 설정을 바꿨다면 최소 변경으로 이분 탐색(하나씩 적용)해서 어떤 값이 문제인지 찾기
2.2 프롬프트가 정책 위반으로 해석되는 경우
의도는 “보안 점검”인데, 모델은 “불법 행위 가이드”로 해석할 수 있습니다. 예를 들어:
- “SQL 인젝션 페이로드를 다양하게 만들어줘”
- “악성코드 샘플 코드를 작성해줘”
이 경우는 400 또는 4xx로 떨어지면서, 메시지에 정책 관련 힌트가 포함될 수 있습니다.
대응
- 목적을 명확히: “방어 목적”, “취약점 점검 체크리스트”, “탐지/차단 규칙” 중심으로 재작성
- 출력 형식을 제한: 공격 실행 코드 대신 탐지 룰/보안 권고안 형태로 유도
2.3 구조화 출력 스키마가 ‘위험한 출력’을 강제하는 경우
예를 들어 responseSchema에 weapon_instructions 같은 필드를 강제(required)하면, 모델이 정책을 지키려다 스키마를 만족 못해 실패하거나, 요청 자체가 거절될 수 있습니다.
대응
- 위험 가능성이 있는 필드는 optional로 바꾸고, 안전한 대체 필드(예:
risk_assessment,mitigation_steps)를 제공 required를 최소화하고, “출력 불가 시 null 허용” 전략을 적용
3) 스키마/요청 바디 오류로 400이 나는 패턴
여기서 말하는 스키마는 크게 두 가지입니다.
- 요청 JSON의 스펙(프로토콜) 자체:
contents배열,parts타입, tool 호출 구조 등 - responseSchema(JSON Schema): 모델이 맞춰야 할 출력 구조
3.1 contents/parts 구조가 잘못됨
가장 흔한 실수는 “문자열을 바로 넣는 것”입니다.
잘못된 예(의사 코드)
await client.models.generateContent({
model: "gemini-1.5-pro",
contents: "hello" // ❌ 배열/parts 구조가 아님
});
올바른 예(REST에 가까운 형태)
{
"model": "gemini-1.5-pro",
"contents": [
{
"role": "user",
"parts": [{ "text": "hello" }]
}
]
}
대응
- 요청을 만들 때 “내가 보내는 JSON”을 항상 로그로 남기되, 개인정보/키는 마스킹
- SDK를 쓰더라도 최종적으로 어떤 payload가 나가는지 확인(HTTP 인터셉터/미들웨어)
3.2 responseSchema(JSON Schema)에서 타입/required/nullable 실수
구조화 출력에서 400을 만드는 1순위는 JSON Schema의 타입 불일치입니다.
type: "object"인데properties가 없음required에 정의되지 않은 키가 들어감- 배열인데
items가 없음 additionalProperties처리 미흡
예: 견고한 responseSchema
{
"responseMimeType": "application/json",
"responseSchema": {
"type": "object",
"properties": {
"answer": { "type": "string" },
"citations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": { "type": "string" },
"url": { "type": "string" }
},
"required": ["title", "url"],
"additionalProperties": false
}
},
"confidence": { "type": "number" }
},
"required": ["answer"],
"additionalProperties": false
}
}
실전 팁
- 처음에는
required를 최소로 두고, 점진적으로 강화 additionalProperties: false를 켜면 모델이 엉뚱한 키를 내는 것을 막지만, 반대로 스키마가 조금만 빡빡해도 실패 확률이 올라갑니다. 운영에서는 “허용 폭”을 적절히 둡니다.
3.3 tool/function calling 스키마 불일치
도구 호출을 붙이면 또 다른 스키마가 등장합니다.
- 함수 파라미터 JSON Schema가 잘못됨
- 모델이 만든 arguments가 스키마를 만족 못함
대응
- 함수 파라미터 스키마에
enum,minLength,pattern같은 제약을 과도하게 넣지 말고, 서버에서 2차 검증 - 모델 출력이 스키마를 못 맞추면 “재시도 프롬프트(validator feedback)”로 교정
4) 재현 가능한 디버깅: 요청 검증 + 에러 파싱 코드
운영에서 중요한 건 “400이 났다”가 아니라 어떤 필드가 invalid인지 자동으로 뽑아내는 것입니다.
아래는 Node.js(Express/Fetch) 기준으로, 요청 payload를 검증하고 에러를 구조화해 로깅하는 예시입니다.
4.1 Zod로 요청 바디 사전 검증(클라이언트 측)
import { z } from "zod";
const PartSchema = z.object({
text: z.string().min(1)
});
const ContentSchema = z.object({
role: z.enum(["user", "model"]),
parts: z.array(PartSchema).min(1)
});
const GeminiRequestSchema = z.object({
model: z.string().min(1),
contents: z.array(ContentSchema).min(1),
// 선택 필드들은 점진적으로 추가
generationConfig: z
.object({
temperature: z.number().min(0).max(2).optional(),
maxOutputTokens: z.number().int().positive().optional()
})
.optional()
});
export function assertGeminiRequest(payload: unknown) {
const parsed = GeminiRequestSchema.safeParse(payload);
if (!parsed.success) {
throw new Error(
`Invalid request payload before calling Gemini: ${parsed.error.message}`
);
}
return parsed.data;
}
이렇게 하면 “Gemini가 400을 주기 전에” 우리 코드에서 구조 오류를 차단할 수 있습니다.
4.2 Gemini 400 에러에서 fieldViolations 뽑아내기
type FieldViolation = { field?: string; description?: string };
export async function callGemini(url: string, apiKey: string, payload: any) {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": apiKey
},
body: JSON.stringify(payload)
});
const text = await res.text();
let body: any;
try {
body = JSON.parse(text);
} catch {
body = { raw: text };
}
if (!res.ok) {
const violations: FieldViolation[] =
body?.error?.details
?.flatMap((d: any) => d?.fieldViolations ?? [])
?.map((v: any) => ({ field: v.field, description: v.description })) ??
[];
const err = new Error(
`Gemini API error ${res.status}: ${body?.error?.message ?? "unknown"}`
);
(err as any).httpStatus = res.status;
(err as any).violations = violations;
(err as any).responseBody = body;
throw err;
}
return body;
}
로그에는 최소한 아래를 남기면 좋습니다.
httpStatuserror.messageviolations[]- 요청의 “스키마 관련 필드”만 샘플링(전체 프롬프트는 개인정보 이슈)
5) 운영에서의 재시도/폴백 전략
400은 보통 “재시도해도 안 되는 오류”로 분류되지만, 구조화 출력/도구 호출에서는 예외가 있습니다.
- 모델이 스키마를 못 맞춘 경우: validator feedback을 주고 1~2회 재시도
- Safety로 막힌 경우: 프롬프트를 안전한 형태로 변환(방어적 목적 명시) 후 재시도
- 그래도 안 되면: 구조화 출력 요구를 제거하고 자연어 응답으로 폴백
이때 중요한 건 “무한 재시도”를 막는 것입니다. Cloud Run 같은 환경에서는 재시도 폭주가 503/콜드스타트 문제로 번지기도 합니다. 트래픽 급증 시 안정화 관점은 GCP Cloud Run 503·콜드스타트 폭증 해결 가이드에서 다룬 방식(큐잉/동시성/타임아웃/리트라이 상한)이 그대로 적용됩니다.
6) 흔한 원인별 빠른 처방전
6.1 responseMimeType/responseSchema 붙였더니 400
responseMimeType을application/json으로 설정했는데 스키마가 불완전required가 과도하거나additionalProperties: false로 인해 모델이 조금만 벗어나도 실패
처방
required최소화 → 2)additionalProperties완화 → 3) 스키마 점진 강화
6.2 Safety 설정을 “최대한 풀었는데” 400
- 허용되지 않는 threshold 조합, 혹은 SDK에서 지원하지 않는 값
처방
- Safety 설정을 일단 제거하고 정상 호출 확인 → 그다음 한 항목씩 추가
6.3 프롬프트는 평범한데 간헐적으로 400
- 사용자 입력이 섞이면서 특정 단어/문맥에서 정책 위반으로 해석
- 혹은 사용자 입력이 스키마를 깨는 형태(예: 너무 긴 문자열, 특수문자 폭주)
처방
- 사용자 입력 정규화/길이 제한
- “정책 위반 가능 입력”을 서버에서 사전 필터링
- 요청/응답 로깅에 사용자 입력의 해시(원문 저장 대신) 남기기
7) 결론: 400을 ‘모호한 에러’로 두지 않는 방법
Gemini API의 400 Invalid Argument는 크게 안전필터와 스키마/요청 형식으로 나뉘며, 둘을 섞어서 디버깅하면 시간이 오래 걸립니다. 해결의 핵심은 다음 3가지입니다.
- 요청 payload를 사전 검증(Zod/JSON Schema validator)
- Gemini 에러의 fieldViolations를 구조화 로깅
- 구조화 출력/도구 호출은 점진적으로 스키마를 강화하고, 필요 시 폴백/재시도 전략을 둔다
이 3가지만 갖춰도 400은 “운영에서 가장 귀찮은 에러”가 아니라, 원인이 즉시 드러나는 에러로 바뀝니다.