API: a time with no offset makes interviews answer 500 and moves a reminder into the server's zone #567
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#567
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, by reading the API's input schemas and posting times without an offset through the test client.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atapi/api.py:398-412(/me); the schemas, the routers and the service are where they were.Every time the API accepts is declared as a plain
dt.datetime, so pydantic turns"2026-11-02T10:00:00"into a naive datetime, and nothing afterwards refuses it or makes it aware. Where a service compares it withtimezone.now(), the call crashes with a 500. Where it goes straight into the database, Django reads it insettings.TIME_ZONEand saves it with a warning. That is not the token owner's zone, nor the default set in Server settings, and the response echoes the naive value back, so the client never sees the shift.api/schemas.py:394-413:InterviewIn.starts_at/ends_atandInterviewPatch.starts_at/ends_at.api/schemas.py:504-532:EventIn.occurred_at,ReminderIn.due_atandReminderPatch.due_at.api/schemas.py:243:ListingEventIn.occurred_at.api/routers/interviews.py:109-155:add_interviewandchange_interviewcompare the times (:115,:146) and pass them toschedule_interview/reschedule_interview.applications/services.py:409-410:if ends_at < now.api/routers/reminders.py:44-47:add_remindercreates the row with the raw value.POST /api/v1/interviewswith"starts_at": "2026-11-02T10:00:00"raisesTypeError: can't compare offset-naive and offset-aware datetimesatservices.py:410and answers the HTML 500 page rather than a problem document.PATCH /api/v1/interviews/{id}fails the same way, as does a POST that mixes an aware and a naive time (interviews.py:115).POST /api/v1/reminderswith"due_at": "2026-11-02T10:00:00"answers 201 and echoes"2026-11-02T10:00:00". The row holds 09:00 UTC, which is 04:00 in New York, so a reminder asked for at 10:00 by somebody in New York fires six hours early.POST /api/v1/applications/{id}/eventsand the listing-history call storeoccurred_atthe same way.add_reminder(src/postulo_mcp/server.py:290-294) passes the model's string straight through. Its docstring suggests aZ, but nothing enforces one./me(api/api.py:396-410) says nothing about the person's zone, so a client has nothing to build an aware moment from.AwareDatetime, so a missing offset is a 422 problem document that names the field. Do the same for theupdated_sincequery parameters. If local times are to be accepted instead, make them aware in the token owner's zone (profile.time_zone, elsesite.default_time_zone()) in one shared validator, never insettings.TIME_ZONE. Either way, returntime_zonefrom/meso a client can send the right offset.Done when
tests/test_api.py: astarts_atwith no offset onPOSTandPATCH /api/v1/interviewsis a 422 problem document naming the field, never a 500. Adue_atwith no offset onPOST /api/v1/remindersis refused the same way, or stored as 10:00 in the owner's zone.