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:
Claudio
2026-05-10 22:32:44 +02:00
parent 63dd39d172
commit 89f99d220a

View File

@@ -2,7 +2,7 @@
Photos API router
"""
from typing import List, Optional, Dict, Any
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from fastapi.responses import FileResponse
@@ -888,6 +888,15 @@ async def update_photo(
if 'taken_at' in update_data:
new_dt = update_data.pop('taken_at')
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:
await write_taken_at(photo.filepath, new_dt)
except ExifWriteError as exc: