API: an unknown address, a non-numeric id or a wrong method is answered in HTML, not as a problem document #430
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#430
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, probing addresses under/api/v1/with the test client, with and without a token.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atapi/api.py:829-844(urls());api/problems.pyandconfig/urls.py:30have not moved, and the test client gives the same four answers.problems.installturns into problem documents the refusals that django-ninja's operations raise. It cannot reach the ones that happen before an operation is chosen. So a mistyped address, an id that is not a number, and a method a known address does not take are all answered by Django or ninja in their own shape: text/html. The wiki says every refusal is a problem document, and a client generated from the OpenAPI description has no branch for HTML.Where:
api/problems.py:162-238,install: handlers forHttp404,RequestDataTooBig,HttpErrorandValidationErrorraised inside operations only.api/api.py:827-842,urls(), andconfig/urls.py:30: nothing catches an address underapi/v1/that matches no pattern, including an id that fails the{int:pk}converter.api/problems.py:76-88,PHRASES, lists 405 although its comment says it holds only the codes this API actually refuses with. Nothing produces a 405 problem document.postulo.wiki/The-capture-API.md:475-477: "Every one of them is a problem document …Content-Type: application/problem+json".What happens: answers observed with the test client:
GET /api/v1/nothing-heretext/html; charset=utf-8GET /api/v1/applications/abctext/html; charset=utf-8DELETE /api/v1/applicationstext/html; charset=utf-8Method not allowedGET /api/v1/applications/99999application/problem+jsonThe first three are the same with or without a token. The last row shows the intended behaviour.
Why it matters: a client matching
+jsonand readingtype/detailgets HTML. Its error path fails on parsing the body rather than reporting "no such address" or "wrong method", and those are the mistakes a client author makes first. The same answers without a token also fall short of the threat model's "the API answers401to everything without a live token" (docs/THREAT-MODEL.md:13). They reveal only the route table, which the source and the wiki already publish, so this is a consistency gap rather than a disclosure.Fix: in
urls(), append a catch-all pattern afterapi.urls(for examplere_path(r"^.*$", …)inside theapi/v1/include) whose view answersproblems.document(request, 404)asapplication/problem+json, or a 401 problem document when the request has no live token. Wrap ninja'sHttpResponseNotAllowedthe same way, either by overriding the API's method-not-allowed response or by post-processing it in the viewurls()returns, keeping theAllowheader. ThenPHRASES[405]is true.Related to #296 (closed), whose "Done when" asked for refusals from every raise site to come back as problem documents. These three come from outside the operations it covered.
Done when
tests/test_api_problems.pyhas a test asserting thatGET /api/v1/nothing-here,GET /api/v1/applications/abcandDELETE /api/v1/applicationsanswerapplication/problem+jsonwith the rightstatus, and that the last one keeps itsAllowheader. It fails today.