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를 호출할 때 다음과 같은 에러가 발생했다.
{
"error": {
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"status": "RESOURCE_EXHAUSTED"
}
}
처음에는 일반적인 quota 문제처럼 보였다. 하지만 확인해보니 상황이 달랐다.
curl 호출은 정상"hi" 같은 텍스트만 넣으면 정상gemini-3.1-flash-image-preview에서는 이미지 URL 입력이 정상gemini-3.5-flash에서만 외부 이미지 URL을 fileData.fileUri로 넣으면 실패즉, API key나 프로젝트 quota 문제보다는 payload, 특히 외부 이미지 URL 처리 문제로 좁혀졌다.
최종적으로 사용한 payload는 대략 이런 형태였다.
{
"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다. <citation refs="D6ZdWg2vjuYNLrMlhg1-x">“Supported data types… Inputs Text, Image, Video, Audio, and PDF… Output Text”</citation>
또한 Gemini API는 외부 HTTPS URL을 fileData.fileUri로 직접 넘기는 방식을 지원한다. 문서상 Gemini API가 처리 중 해당 URL을 fetch하며, 단 Gemini 2.0 계열은 이 기능을 지원하지 않는다고 되어 있다. <citation refs="0cth2gU7Z8smHjH5gNqtl">“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”</citation>
이미지 타입도 image/jpeg, image/png, image/webp 등이 지원된다. <citation refs="0cth2gU7Z8smHjH5gNqtl">“Image file types… image/bmp… image/jpeg… image/png… image/webp”</citation>
Cloud 문서에서도 fileData.fileUri에 HTTP URL을 넣을 수 있고, 이 URL은 publicly readable 해야 한다고 설명한다. <citation refs="nH6Iacut--nSdScLgItM8">“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”</citation>
따라서 문서상으로는 다음 조합이 가능해야 한다.
gemini-3.5-flash
+ fileData.fileUri
+ public HTTPS image URL
+ image/webp
핵심은 외부 URL을 누가 가져오느냐다.
fileData.fileUri를 쓰면 내 서버가 이미지를 Gemini에 직접 보내는 것이 아니다. Gemini backend가 해당 URL을 직접 fetch한다.
따라서 내 서버나 브라우저에서 URL이 잘 열려도, Gemini backend에서는 실패할 수 있다.
가능한 원인은 다음과 같다.
실제로 @google/genai GitHub 이슈에도, 같은 presigned S3 URL과 같은 prompt인데도 Gemini 쪽에서 간헐적으로 Cannot fetch content from the provided URL이 발생하고, retry하면 성공하는 사례가 보고되어 있다. <citation refs="rCnwydWk9RywusbFpLaHM">“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.”</citation>
즉, 외부 URL fetch는 항상 deterministic하게 성공한다고 보기 어렵다.
공식 문서가 “gemini-3.1-flash-image-preview와 gemini-3.5-flash는 서로 다른 URL fetch path를 쓴다”고 직접 말하진 않는다.
하지만 다음 사실은 확인된다.
gemini-3.5-flash는 이미지 입력은 지원하지만 이미지 생성은 지원하지 않는 text-output 모델이다. <citation refs="D6ZdWg2vjuYNLrMlhg1-x">“Output Text… Image generation Not supported”</citation>
반면 gemini-3.1-flash-image 계열은 이미지 생성 및 편집용 모델이고, 입력은 Text and Image/PDF, 출력은 Image and Text다. <citation refs="U9qsv7VvUFUepT3_VGnkh">“Supported data types… Inputs Text and Image / PDF… Output Image and Text… Image generation Supported”</citation>
즉 두 모델은 모두 이미지 입력을 받을 수 있지만, 모델의 목적과 processing pipeline은 다를 수 있다.
따라서 다음과 같이 정리하는 게 정확하다.
gemini-3.5-flash가 외부 이미지 URL을 지원하지 않는 것은 아니다. 문서상 지원한다.
하지만 실제 운영에서는 모델별, endpoint별, URL host별로 외부 URL fetch 실패가 발생할 수 있고, 이 실패가 generic429 RESOURCE_EXHAUSTED로 보일 수 있다.
정상적이라면 URL fetch 실패는 400 INVALID_ARGUMENT나 Cannot fetch content from the provided URL 같은 에러로 나오는 것이 자연스럽다.
그런데 이번 케이스는 다음처럼 generic 429가 나왔다.
{
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"status": "RESOURCE_EXHAUSTED"
}
이 때문에 처음에는 quota 문제처럼 보인다.
하지만 다음 조건이 모두 맞다면 quota보다는 URL retrieval 문제를 먼저 의심해야 한다.
await ai.models.generateContent({
model: "gemini-3.5-flash",
contents: "hi",
});
이게 성공하면 SDK, key, project 자체는 정상이다.
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",
},
},
],
},
],
});
결과 해석:
gemini-3.5-flash + external URL 경로 문제 가능성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에 넣는 방식은 편하지만, 운영 안정성은 떨어질 수 있다.
권장 순서는 다음과 같다.
server fetch
-> content-type 확인
-> base64 변환
-> inlineData로 전달
server fetch
-> Gemini File API upload
-> 반환된 file.uri를 fileData.fileUri로 전달
임의의 외부 CDN URL
-> fileData.fileUri에 직접 전달
특히 다음 출처의 이미지는 직접 URL 전달을 피하는 것이 좋다.
이번 문제는 단순 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로 전달하는 방식이 가장 안전하다.