From 89f99d220a1b8f4704928e431c3e19f4f936d141 Mon Sep 17 00:00:00 2001 From: Claudio Date: Sun, 10 May 2026 22:32:44 +0200 Subject: [PATCH] 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) --- backend/app/routers/photos.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/app/routers/photos.py b/backend/app/routers/photos.py index 9c76500..aecc977 100644 --- a/backend/app/routers/photos.py +++ b/backend/app/routers/photos.py @@ -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 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: