fix(photos): coerce tz-aware taken_at to naive UTC before DB write
PATCH /api/v1/photos/{id} returned 500 with
'can't subtract offset-naive and offset-aware datetimes' when the
frontend sent a tz-aware taken_at value (e.g. 2026-05-09T00:12+02:00).
The photos.taken_at column is timestamp without time zone, so asyncpg
refuses to bind a tz-aware datetime.
The frontend's datetime-local input is supposed to be naive but real-
world locales / browsers / paste flows occasionally include offsets.
Normalize on the server: if tzinfo is present, convert to UTC and drop
the tzinfo so both shapes round-trip cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
Photos API router
|
Photos API router
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
@@ -888,6 +888,15 @@ async def update_photo(
|
|||||||
if 'taken_at' in update_data:
|
if 'taken_at' in update_data:
|
||||||
new_dt = update_data.pop('taken_at')
|
new_dt = update_data.pop('taken_at')
|
||||||
if new_dt is not None:
|
if new_dt is not None:
|
||||||
|
# The frontend's <input type="datetime-local"> usually serializes
|
||||||
|
# without a tz, but a manual edit / paste / certain locales can
|
||||||
|
# send a tz-aware ISO (e.g. "2026-05-09T00:12+02:00"). The
|
||||||
|
# photos.taken_at column is `timestamp without time zone`, so
|
||||||
|
# asyncpg can't bind a tz-aware value — it raises
|
||||||
|
# "can't subtract offset-naive and offset-aware datetimes".
|
||||||
|
# Normalize to naive UTC so both shapes round-trip cleanly.
|
||||||
|
if new_dt.tzinfo is not None:
|
||||||
|
new_dt = new_dt.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
try:
|
try:
|
||||||
await write_taken_at(photo.filepath, new_dt)
|
await write_taken_at(photo.filepath, new_dt)
|
||||||
except ExifWriteError as exc:
|
except ExifWriteError as exc:
|
||||||
|
|||||||
Reference in New Issue
Block a user