Interviews API: a PATCH with an explicit null answers 500 #436
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#436
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, comparing how the threePATCHcalls read their payloads and sending each interview field asnullthrough the test client.Checked again on 2026-10-01 against
mainat0a5cad587: still present.InterviewPatchdeclares every fieldX | None = None, andchange_interviewreads it withpayload.dict(exclude_unset=True), which drops the fields a client left out but keeps the ones it sent asnull. Three of them then crash rather than being refused: the client gets Django's HTML error page with a 500 instead of a problem document.patch_companyskipsNonevalues, andchange_reminderskips them wherevernullhas no meaning of its own; this call does not.api/routers/interviews.py:136-154,change_interview;api/schemas.py:407-413,InterviewPatch.{"starts_at": null}: line 142 takesNoneas the new start, and line 144 computesNone + interview.duration, aTypeError: 500.{"location": null}or{"notes": null}: line 149 setsNoneon aNOT NULLtext column andinterview.save()raisesIntegrityError: 500.{"kind": null}is refused with a 422 bychoice_or_422;{"ends_at": null}and{"contact_ids": null}are treated as "unchanged", which is whatpatch_companydoes for every field.nullfor "leave it", gets a crash for an ordinary request, and each one is logged as a server error. The same call treatsnullthree different ways depending on the field.Nonevalues afterpayload.dict(exclude_unset=True), aspatch_companydoes (api/routers/companies.py:94-96), sonullmeans "unchanged" for every field; or refuse anullwith a 422 naming the field. Either way, document it onInterviewPatch.Done when
tests/test_api.pysendsPATCH /api/v1/interviews/{id}with each field ofInterviewPatchset tonull, parametrised over the fields, and asserts a 200 (or a 422 problem document) and an unchanged interview, never a 500.