API: an unknown address, a non-numeric id or a wrong method is answered in HTML, not as a problem document #430

Closed
opened 2026-10-01 10:46:39 +00:00 by tiagoagueda · 0 comments
Owner

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 main at 0a5cad587: still present. The code is now at api/api.py:829-844 (urls()); api/problems.py and config/urls.py:30 have not moved, and the test client gives the same four answers.

problems.install turns 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 for Http404, RequestDataTooBig, HttpError and ValidationError raised inside operations only.
    • api/api.py:827-842, urls(), and config/urls.py:30: nothing catches an address under api/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:

    Request Status Content-Type Body
    GET /api/v1/nothing-here 404 text/html; charset=utf-8 the site's HTML error page
    GET /api/v1/applications/abc 404 text/html; charset=utf-8 the site's HTML error page
    DELETE /api/v1/applications 405 text/html; charset=utf-8 Method not allowed
    GET /api/v1/applications/99999 401 without a token, 404 with one application/problem+json a problem document

    The first three are the same with or without a token. The last row shows the intended behaviour.

  • Why it matters: a client matching +json and reading type/detail gets 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 answers 401 to 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 after api.urls (for example re_path(r"^.*$", …) inside the api/v1/ include) whose view answers problems.document(request, 404) as application/problem+json, or a 401 problem document when the request has no live token. Wrap ninja's HttpResponseNotAllowed the same way, either by overriding the API's method-not-allowed response or by post-processing it in the view urls() returns, keeping the Allow header. Then PHRASES[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.py has a test asserting that GET /api/v1/nothing-here, GET /api/v1/applications/abc and DELETE /api/v1/applications answer application/problem+json with the right status, and that the last one keeps its Allow header. It fails today.
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 `main` at `0a5cad587`: still present. The code is now at `api/api.py:829-844` (`urls()`); `api/problems.py` and `config/urls.py:30` have not moved, and the test client gives the same four answers. `problems.install` turns 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 for `Http404`, `RequestDataTooBig`, `HttpError` and `ValidationError` raised inside operations only. - `api/api.py:827-842`, `urls()`, and `config/urls.py:30`: nothing catches an address under `api/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: | Request | Status | Content-Type | Body | | --- | --- | --- | --- | | `GET /api/v1/nothing-here` | 404 | `text/html; charset=utf-8` | the site's HTML error page | | `GET /api/v1/applications/abc` | 404 | `text/html; charset=utf-8` | the site's HTML error page | | `DELETE /api/v1/applications` | 405 | `text/html; charset=utf-8` | `Method not allowed` | | `GET /api/v1/applications/99999` | 401 without a token, 404 with one | `application/problem+json` | a problem document | The first three are the same with or without a token. The last row shows the intended behaviour. - **Why it matters:** a client matching `+json` and reading `type`/`detail` gets 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 answers `401` to 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 after `api.urls` (for example `re_path(r"^.*$", …)` inside the `api/v1/` include) whose view answers `problems.document(request, 404)` as `application/problem+json`, or a 401 problem document when the request has no live token. Wrap ninja's `HttpResponseNotAllowed` the same way, either by overriding the API's method-not-allowed response or by post-processing it in the view `urls()` returns, keeping the `Allow` header. Then `PHRASES[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.py` has a test asserting that `GET /api/v1/nothing-here`, `GET /api/v1/applications/abc` and `DELETE /api/v1/applications` answer `application/problem+json` with the right `status`, and that the last one keeps its `Allow` header. It fails today.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:39 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#430
No description provided.