Skip to content

Instantly share code, notes, and snippets.

@matsubo
Last active December 4, 2025 07:02
Show Gist options
  • Select an option

  • Save matsubo/c1366c36b1045484a00d0756ab082f3a to your computer and use it in GitHub Desktop.

Select an option

Save matsubo/c1366c36b1045484a00d0756ab082f3a to your computer and use it in GitHub Desktop.
#!/bin/bash
# Please ensure GOOGLE_API_KEY is set.
# The key can be found: https://aistudio.google.com/apps
# Create test.xlsx file
echo "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" | base64 -d > test.xlsx
# Download PoC script
curl https://gist.githubusercontent.com/matsubo/c1366c36b1045484a00d0756ab082f3a/raw/poc_mime_type_issue.py > poc_mime_type_issue.py
# Execute
python poc_mime_type_issue.py
"""
Proof of Concept: MIME Type Validation issue in uploadToFileSearchStore API
This script demonstrates a server-side validation issue where the API rejects a
valid MIME type for Excel files (.xlsx).
Error: 400 INVALID_ARGUMENT
Message: "When provided, MIME type must be in a valid type/subtype format"
MIME Type Used: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Issue: The MIME type IS in valid type/subtype format, but the API incorrectly rejects it.
"""
from google import genai
from google.genai import types
import time
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Get API key
api_key = os.getenv('GOOGLE_API_KEY')
if not api_key:
raise ValueError("GOOGLE_API_KEY not found in environment variables")
client = genai.Client(api_key=api_key)
print("=" * 80)
print("PoC: MIME Type issue in uploadToFileSearchStore API")
print("=" * 80)
# Preparation: Create a file search store
print("\nPreparation: Creating file search store...")
file_search_store = client.file_search_stores.create(
config={'display_name': 'poc-mime-type-xlsx'}
)
print(f" Created: {file_search_store.name}")
# Step 1: Attempt to upload an Excel file with the correct MIME type
print("\n1. Uploading Excel file (.xlsx) with correct MIME type...")
test_file = "test.xlsx"
mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
print(f" File: {test_file}")
print(f" MIME Type: {mime_type}")
print(f" Note: This MIME type is the official IANA-registered type for .xlsx files")
try:
with open(test_file, 'rb') as f:
operation = client.file_search_stores.upload_to_file_search_store(
file=f,
file_search_store_name=file_search_store.name,
config={
'display_name': 'test.xlsx',
'mime_type': mime_type # VALID MIME TYPE
}
)
# Wait for completion
while not operation.done:
time.sleep(2)
operation = client.operations.get(operation)
print("\n ✓ SUCCESS: File uploaded successfully")
except Exception as e:
print("\n ✗ ERROR: Upload failed")
print(f"\n{e}")
print("\n" + "=" * 80)
print("ISSUE CONFIRMED")
print("=" * 80)
print("The API incorrectly rejects the MIME type:")
print(f" {mime_type}")
print("\nThis is a valid IANA-registered MIME type in 'type/subtype' format.")
print("Expected: API should accept this MIME type")
print("Actual: API returns 400 INVALID_ARGUMENT")
print("=" * 80)
# Step 2: Try without mime_type to see if it works
print("\n\n2. Testing upload WITHOUT mime_type parameter...")
try:
with open(test_file, 'rb') as f:
operation = client.file_search_stores.upload_to_file_search_store(
file=f,
file_search_store_name=file_search_store.name,
config={
'display_name': 'test-without-mimetype.xlsx'
# NO mime_type specified
}
)
# Wait for completion
while not operation.done:
time.sleep(2)
operation = client.operations.get(operation)
print(" ✓ SUCCESS: File uploaded without mime_type parameter")
print("\n This confirms the issue: The API works when mime_type is omitted,")
print(" but fails when the correct mime_type is provided.")
except Exception as e:
print(f"\n ✗ Also failed without mime_type: {e}")
# Step 3: Try alternative approach - upload via File API then import
print("\n\n3. Testing alternative approach: File API upload → import to store...")
print(" This tests if using client.files.upload() + import_file() works better")
try:
# Upload file using File API
with open(test_file, 'rb') as f:
file_ref = client.files.upload(
file=f,
config=types.UploadFileConfig(
display_name='test-via-file-api.xlsx',
mime_type=mime_type
)
)
print(f" ✓ Uploaded via File API: {file_ref.name}")
# Import the uploaded file into the file search store
import_op = client.file_search_stores.import_file(
file_search_store_name=file_search_store.name,
file_name=file_ref.name,
)
print(f" File import started: {import_op.name}")
print(" Waiting for import to complete", end="")
while not (import_op := client.operations.get(import_op)).done:
time.sleep(1)
print(".", end="", flush=True)
print()
print(" ✓ SUCCESS: File imported successfully via File API → import_file()")
print("\n This approach works! Use File API upload + import as a workaround.")
except Exception as e:
print(f"\n ✗ Failed with File API approach: {e}")
print("\n" + "=" * 80)
print("PoC Complete")
print("=" * 80)
print("\nSUMMARY:")
print(" Step 1 (direct upload with mime_type): Expected to fail")
print(" Step 2 (direct upload without mime_type): May work")
print(" Step 3 (File API + import): Recommended workaround")
print("=" * 80)
@matsubo
Copy link
Author

matsubo commented Dec 4, 2025

Output

❯ GOOGLE_API_KEY=xxxxxxxxxxxxx uv run python poc_mime_type_issue.py
================================================================================
PoC: MIME Type issue in uploadToFileSearchStore API
================================================================================

Preparation: Creating file search store...
   Created: fileSearchStores/pocmimetypexlsx-34khecxjwyia

1. Uploading Excel file (.xlsx) with correct MIME type...
   File: test.xlsx
   MIME Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
   Note: This MIME type is the official IANA-registered type for .xlsx files

   ✗ ERROR: Upload failed

400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': "* UploadToFileSearchStoreRequest.mime_type: When provided, MIME type must be in a valid type/subtype format (e.g., 'text/plain', 'application/pdf').\n", 'status': 'INVALID_ARGUMENT'}}

================================================================================
ISSUE CONFIRMED
================================================================================
The API incorrectly rejects the MIME type:
  application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

This is a valid IANA-registered MIME type in 'type/subtype' format.
Expected: API should accept this MIME type
Actual: API returns 400 INVALID_ARGUMENT
================================================================================


2. Testing upload WITHOUT mime_type parameter...

   ✗ Also failed without mime_type: Unknown mime type: Could not determine the mimetype for your file
 please set the `mime_type` argument


3. Testing alternative approach: File API upload → import to store...
   This tests if using client.files.upload() + import_file() works better
   ✓ Uploaded via File API: files/u129b2vloblf
   File import started: fileSearchStores/pocmimetypexlsx-34khecxjwyia/operations/u129b2vloblf-fxng7npnzfdz
   Waiting for import to complete.
   ✓ SUCCESS: File imported successfully via File API → import_file()

   This approach works! Use File API upload + import as a workaround.

================================================================================
PoC Complete
================================================================================

SUMMARY:
  Step 1 (direct upload with mime_type): Expected to fail
  Step 2 (direct upload without mime_type): May work
  Step 3 (File API + import): Recommended workaround
================================================================================

pakcage version

❯ uv run pip list
Package                 Version    Editable project location
----------------------- ---------- --------------------------------------------------------------------
annotated-types         0.7.0
anyio                   4.12.0
cachetools              6.2.2
certifi                 2025.11.12
charset-normalizer      3.4.4
cobble                  0.1.4
google-auth             2.43.0
google-genai            1.53.0
h11                     0.16.0
httpcore                1.0.9
httpx                   0.28.1
idna                    3.11
mammoth                 1.11.0
pip                     24.3.1
pyasn1                  0.6.1
pyasn1_modules          0.4.2
pydantic                2.12.5
pydantic_core           2.41.5
python-dotenv           1.2.1
requests                2.32.5
rsa                     4.9.1
tenacity                9.1.2
typing_extensions       4.15.0
typing-inspection       0.4.2
urllib3                 2.5.0
websockets              15.0.1
xlsx2csv                0.8.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment