Skip to content

Commit fbd48e3

Browse files
Fix storage upload pipeline: proper URL encoding, no silent failures
- storage_client: use urllib.parse.quote(path, safe='/') for all URL construction, so any filename (spaces, unicode, special chars) works - storage_client: capture and log Supabase response body on HTTP errors - storage_client: raise on failure in list_files and remove_files instead of returning [] or swallowing errors - server.py: revert space→underscore hack in _storage_path — URL encoding at the HTTP layer is the correct fix - server.py: wrap cleanup remove_files call so original error is preserved even if cleanup also fails
1 parent 6bd47d7 commit fbd48e3

2 files changed

Lines changed: 29 additions & 11 deletions

File tree

codex-backend/server.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,9 @@ def verify_thread_ownership(thread_id: str, user_id: str):
8989

9090

9191
def _storage_path(user_id: str, filename: str, thread_id: str | None = None) -> str:
92-
safe_name = filename.replace(" ", "_")
9392
if thread_id:
94-
return f"{user_id}/{thread_id}/{safe_name}"
95-
return f"{user_id}/{safe_name}"
93+
return f"{user_id}/{thread_id}/{filename}"
94+
return f"{user_id}/{filename}"
9695

9796

9897
async def _download_from_storage(storage_path: str, auth_token: str | None = None) -> str | None:
@@ -382,16 +381,18 @@ async def upload_document(file: UploadFile = File(...), current_user=Depends(get
382381
tmp_path = await _download_from_storage(storage_path, auth_token=current_user.token)
383382
if not tmp_path:
384383
raise RuntimeError("Failed to retrieve uploaded file for ingestion")
385-
safe_filename = os.path.basename(storage_path)
386-
renamed = os.path.join(os.path.dirname(tmp_path), safe_filename)
384+
renamed = os.path.join(os.path.dirname(tmp_path), file.filename)
387385
os.rename(tmp_path, renamed)
388386
await asyncio.to_thread(ingest_file, renamed, None, current_user.id)
389387
os.unlink(renamed)
390388
logger.info(f"Uploaded and ingested: {file.filename}")
391389
return JSONResponse(status_code=200, content={"message": f"Successfully uploaded and ingested {file.filename}"})
392390
except Exception as e:
393391
logger.error(f"Ingestion failed, cleaning up storage file: {e}")
394-
await storage_client.remove_files(STORAGE_BUCKET, [storage_path], auth_token=current_user.token)
392+
try:
393+
await storage_client.remove_files(STORAGE_BUCKET, [storage_path], auth_token=current_user.token)
394+
except Exception:
395+
logger.error(f"Cleanup also failed for storage path: {storage_path}")
395396
return JSONResponse(status_code=500, content={"message": str(e)})
396397

397398

@@ -505,8 +506,7 @@ async def upload_temporal_document(thread_id: str, file: UploadFile = File(...),
505506
tmp_path = await _download_from_storage(storage_path, auth_token=current_user.token)
506507
if not tmp_path:
507508
return JSONResponse(status_code=500, content={"message": "Failed to retrieve uploaded file for ingestion"})
508-
safe_filename = os.path.basename(storage_path)
509-
renamed = os.path.join(os.path.dirname(tmp_path), safe_filename)
509+
renamed = os.path.join(os.path.dirname(tmp_path), file.filename)
510510
os.rename(tmp_path, renamed)
511511
await asyncio.to_thread(ingest_file, renamed, thread_id, current_user.id)
512512
os.unlink(renamed)

codex-backend/src/storage_client.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import urllib.parse
23

34
import httpx
45
from dotenv import load_dotenv
@@ -24,8 +25,12 @@ def _auth_headers(auth_token: str | None = None) -> dict:
2425
}
2526

2627

28+
def _encode_path(path: str) -> str:
29+
return urllib.parse.quote(path, safe="/")
30+
31+
2732
async def upload_file(bucket: str, path: str, data: bytes, content_type: str | None = None, auth_token: str | None = None) -> None:
28-
url = f"{_SUPABASE_URL}/storage/v1/object/{bucket}/{path}"
33+
url = f"{_SUPABASE_URL}/storage/v1/object/{bucket}/{_encode_path(path)}"
2934
headers = _auth_headers(auth_token)
3035
headers["Content-Type"] = content_type or "application/octet-stream"
3136
headers["x-upsert"] = "true"
@@ -34,18 +39,24 @@ async def upload_file(bucket: str, path: str, data: bytes, content_type: str | N
3439
resp = await client.post(url, headers=headers, content=data)
3540
resp.raise_for_status()
3641
logger.info(f"Uploaded to storage: {bucket}/{path}")
42+
except httpx.HTTPStatusError:
43+
logger.error(f"Storage upload failed for {bucket}/{path}: status={resp.status_code} body={resp.text}")
44+
raise
3745
except Exception as e:
3846
logger.error(f"Storage upload failed for {bucket}/{path}: {e}")
3947
raise
4048

4149

4250
async def download_file(bucket: str, path: str, auth_token: str | None = None) -> bytes:
43-
url = f"{_SUPABASE_URL}/storage/v1/object/{bucket}/{path}"
51+
url = f"{_SUPABASE_URL}/storage/v1/object/{bucket}/{_encode_path(path)}"
4452
async with httpx.AsyncClient(timeout=30) as client:
4553
try:
4654
resp = await client.get(url, headers=_auth_headers(auth_token))
4755
resp.raise_for_status()
4856
return resp.content
57+
except httpx.HTTPStatusError:
58+
logger.error(f"Storage download failed for {bucket}/{path}: status={resp.status_code} body={resp.text}")
59+
raise
4960
except Exception as e:
5061
logger.error(f"Storage download failed for {bucket}/{path}: {e}")
5162
raise
@@ -58,9 +69,12 @@ async def list_files(bucket: str, prefix: str, auth_token: str | None = None) ->
5869
resp = await client.post(url, headers=_auth_headers(auth_token), json={"prefix": prefix, "limit": 100, "offset": 0})
5970
resp.raise_for_status()
6071
return resp.json()
72+
except httpx.HTTPStatusError:
73+
logger.error(f"Storage list failed for {bucket}/{prefix}: status={resp.status_code} body={resp.text}")
74+
raise
6175
except Exception as e:
6276
logger.error(f"Storage list failed for {bucket}/{prefix}: {e}")
63-
return []
77+
raise
6478

6579

6680
async def remove_files(bucket: str, paths: list[str], auth_token: str | None = None) -> None:
@@ -69,8 +83,12 @@ async def remove_files(bucket: str, paths: list[str], auth_token: str | None = N
6983
try:
7084
resp = await client.post(url, headers=_auth_headers(auth_token), json={"prefixes": paths})
7185
resp.raise_for_status()
86+
except httpx.HTTPStatusError:
87+
logger.error(f"Storage remove failed for {bucket}/{paths}: status={resp.status_code} body={resp.text}")
88+
raise
7289
except Exception as e:
7390
logger.error(f"Storage remove failed for {bucket}/{paths}: {e}")
91+
raise
7492

7593

7694
async def ensure_bucket(bucket: str) -> None:

0 commit comments

Comments
 (0)