한 줄 요약
gemini-3.5-flash에서 텍스트 호출은 정상인데 fileData.fileUri로 외부 이미지 URL을 넣으면 429 RESOURCE_EXHAUSTED가 나는 경우, 단순 quota 초과가 아니라 **Gemini backend의 외부 URL fetch 실패 또는 모델별 URL ingestion 경로 차이**를 의심해야 한다.
---
문제 상황
Node.js에서 @google/genai SDK로 gemini-3.5-flash를 호출할 때 다음과 같은 에러가 발생했다.
json
{
"error": {
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"status": "RESOURCE_EXHAUSTED"
}
}
처음에는 일반적인 quota 문제처럼 보였다. 하지만 확인해보니 상황이 달랐다.
- 프로젝트는 Tier 3
- 콘솔상 RPM, RPD, TPM 모두 정상
- 같은 API key로 curl 호출은 정상
- SDK에서도 "hi" 같은 텍스트만 넣으면 정상
- gemini-3.1-flash-image-preview에서는 이미지 URL 입력이 정상
- gemini-3.5-flash에서만 외부 이미지 URL을 fileData.fileUri로 넣으면 실패
즉, API key나 프로젝트 quota 문제보다는 **payload, 특히 외부 이미지 URL 처리 문제**로 좁혀졌다.
---
사용한 payload
최종적으로 사용한 payload는 대략 이런 형태였다.
json
{
"contents": [
{
"role": "user",
"parts": [
{
"text": "하이"
},
{
"fileData": {
"mimeType": "image/webp",
"fileUri": "https://i.namu.wiki/i/7XrzAIHYm9NTJ7N56qvYpN8SmbHHpE8q1mSUnQ-kcE64qhFdlHEfsxqL-koOWsNq4g7CrmRNvZSJVH9KkNw8UQ.webp"
}
}
]
}
]
}
처음에는 mimeType을 image/jpeg로 보냈지만, 실제 URL은 .webp였고 응답 헤더도 image/webp였다. MIME을 image/webp로 고쳐도 결과는 동일했다.
---
공식 문서상으로는 가능한 사용법
문서만 보면 gemini-3.5-flash는 이미지 입력을 지원한다. 입력으로 Text, Image, Video, Audio, PDF를 받고, 출력은 Text다. “Supported data types… Inputs Text, Image, Video, Audio, and PDF… Output Text”
또한 Gemini API는 외부 HTTPS URL을 fileData.fileUri로 직접 넘기는 방식을 지원한다. 문서상 Gemini API가 처리 중 해당 URL을 fetch하며, 단 Gemini 2.0 계열은 이 기능을 지원하지 않는다고 되어 있다. “You can pass publicly accessible HTTPS URLs or pre-signed URLs directly in your request. The Gemini API will fetch the content securely during processing… Note: Gemini 2.0 family of models are not supported”
이미지 타입도 image/jpeg, image/png, image/webp 등이 지원된다. “Image file types… image/bmp… image/jpeg… image/png… image/webp”
Cloud 문서에서도 fileData.fileUri에 HTTP URL을 넣을 수 있고, 이 URL은 publicly readable 해야 한다고 설명한다. “HTTP URL: The file URL must be publicly readable. You can specify… up to 10 image files per request… When specifying a fileURI, you must also specify the media type”
따라서 문서상으로는 다음 조합이 가능해야 한다.
txt
gemini-3.5-flash
+ fileData.fileUri
+ public HTTPS image URL
+ image/webp
---
그런데 실제로는 왜 실패할 수 있나
핵심은 **외부 URL을 누가 가져오느냐**다.
fileData.fileUri를 쓰면 내 서버가 이미지를 Gemini에 직접 보내는 것이 아니다. Gemini backend가 해당 URL을 직접 fetch한다.
따라서 내 서버나 브라우저에서 URL이 잘 열려도, Gemini backend에서는 실패할 수 있다.
가능한 원인은 다음과 같다.
- CDN의 hotlink 방지
- User-Agent 기반 차단
- Google backend IP 차단
- region별 접근 제한
- timeout
- redirect 처리 문제
- URL safety check 실패
- WebP 처리 경로 차이
- 모델별 file ingestion 경로 차이
- 일시적인 URL retrieval 장애
실제로 @google/genai GitHub 이슈에도, 같은 presigned S3 URL과 같은 prompt인데도 Gemini 쪽에서 간헐적으로 Cannot fetch content from the provided URL이 발생하고, retry하면 성공하는 사례가 보고되어 있다. “Intermittently receive… Cannot fetch content from the provided URL… Immediately verify the same presigned URL is still valid… Retry the exact same request… often succeeds with no URL/payload changes.”
즉, 외부 URL fetch는 항상 deterministic하게 성공한다고 보기 어렵다.
---
모델별 차이가 실제로 있나?
공식 문서가 “gemini-3.1-flash-image-preview와 gemini-3.5-flash는 서로 다른 URL fetch path를 쓴다”고 직접 말하진 않는다.
하지만 다음 사실은 확인된다.
gemini-3.5-flash는 이미지 입력은 지원하지만 이미지 생성은 지원하지 않는 text-output 모델이다. “Output Text… Image generation Not supported”
반면 gemini-3.1-flash-image 계열은 이미지 생성 및 편집용 모델이고, 입력은 Text and Image/PDF, 출력은 Image and Text다. “Supported data types… Inputs Text and Image / PDF… Output Image and Text… Image generation Supported”
즉 두 모델은 모두 이미지 입력을 받을 수 있지만, 모델의 목적과 processing pipeline은 다를 수 있다.
따라서 다음과 같이 정리하는 게 정확하다.
> gemini-3.5-flash가 외부 이미지 URL을 지원하지 않는 것은 아니다. 문서상 지원한다.
> 하지만 실제 운영에서는 모델별, endpoint별, URL host별로 외부 URL fetch 실패가 발생할 수 있고, 이 실패가 generic 429 RESOURCE_EXHAUSTED로 보일 수 있다.
---
왜 429로 보이나?
정상적이라면 URL fetch 실패는 400 INVALID_ARGUMENT나 Cannot fetch content from the provided URL 같은 에러로 나오는 것이 자연스럽다.
그런데 이번 케이스는 다음처럼 generic 429가 나왔다.
json
{
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"status": "RESOURCE_EXHAUSTED"
}
이 때문에 처음에는 quota 문제처럼 보인다.
하지만 다음 조건이 모두 맞다면 quota보다는 URL retrieval 문제를 먼저 의심해야 한다.
- 텍스트-only 호출은 성공
- 같은 key로 curl 호출 성공
- Tier, RPM, TPM, RPD 모두 정상
- 다른 이미지 모델은 성공
- 특정 모델에서 외부 URL을 넣을 때만 실패
- inlineData로 보내면 성공
---
재현 및 분리 테스트 방법
1. 텍스트만 넣어서 테스트
ts
await ai.models.generateContent({
model: "gemini-3.5-flash",
contents: "hi",
});
이게 성공하면 SDK, key, project 자체는 정상이다.
---
2. Google 공식 샘플 이미지 URL로 테스트
ts
await ai.models.generateContent({
model: "gemini-3.5-flash",
contents: [
{
role: "user",
parts: [
{ text: "What is shown in this image?" },
{
fileData: {
mimeType: "image/jpeg",
fileUri: "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/scones.jpg",
},
},
],
},
],
});
결과 해석:
- 샘플 URL은 성공, 내 URL만 실패
-> URL host/CDN 접근 문제 가능성 높음
- 샘플 URL도 실패
-> gemini-3.5-flash + external URL 경로 문제 가능성
- inlineData는 성공
-> 모델의 vision 기능은 정상, 외부 URL fetch만 문제
---
3. 같은 이미지를 inlineData로 보내기
ts
const imageUrl = "https://example.com/image.webp";
const imageResponse = await fetch(imageUrl);
if (!imageResponse.ok) {
throw new Error(Image fetch failed: ${imageResponse.status});
}
const mimeType =
imageResponse.headers.get("content-type")?.split(";")[0] ?? "image/webp";
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
const result = await ai.models.generateContent({
model: "gemini-3.5-flash",
contents: [
{
role: "user",
parts: [
{ text: "하이" },
{
inlineData: {
mimeType,
data: imageBuffer.toString("base64"),
},
},
],
},
],
});
이 방식은 Gemini backend가 URL을 fetch하지 않는다. 내 서버가 이미지를 받아서 base64로 Gemini에 직접 전달한다.
운영에서는 이 방식이 가장 안정적이다.
---
운영 권장 방식
외부 이미지 URL을 그대로 fileData.fileUri에 넣는 방식은 편하지만, 운영 안정성은 떨어질 수 있다.
권장 순서는 다음과 같다.
작은 이미지
txt
server fetch
-> content-type 확인
-> base64 변환
-> inlineData로 전달
큰 이미지 또는 재사용 이미지
txt
server fetch
-> Gemini File API upload
-> 반환된 file.uri를 fileData.fileUri로 전달
피하는 것이 좋은 방식
txt
임의의 외부 CDN URL
-> fileData.fileUri에 직접 전달
특히 다음 출처의 이미지는 직접 URL 전달을 피하는 것이 좋다.
- namu.wiki
- 커뮤니티 CDN
- 쇼핑몰 CDN
- signed URL
- hotlink 방지 가능성이 있는 이미지 서버
- region별 응답이 달라지는 CDN
- User-Agent에 따라 응답이 달라지는 서버
---
최종 결론
이번 문제는 단순 quota 문제가 아니었다.
gemini-3.5-flash는 문서상 이미지 입력과 외부 URL 입력을 지원한다. 하지만 실제 서비스에서는 fileData.fileUri로 넘긴 외부 URL을 Gemini backend가 직접 fetch해야 하며, 이 과정은 모델별, endpoint별, URL host별로 실패할 수 있다.
특히 gemini-3.1-flash-image-preview에서는 같은 URL이 동작하더라도, gemini-3.5-flash에서 동일하게 동작한다고 보장할 수는 없다.
운영 환경에서는 외부 이미지 URL을 모델에 직접 넘기기보다, 서버에서 이미지를 fetch한 뒤 inlineData로 전달하는 방식이 가장 안전하다.