OpenAPI description: refusals some calls do make are missing (409 and 413 on POST /captures, 404 on two creates) #431

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, comparing the raise sites with what api.get_openapi_schema() describes for each call.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at api/problems.py:303-320 (_statuses, which since #248 is handed the scoped guard rather than importing it), api/api.py:415-450 (create_capture) and api/api.py:673-681 (the responses _A_RENDERING declares); the _too_big handler, api/idempotency.py:99 and the two routers have not moved, and the probe gives the same lists.

problems._statuses works out each call's refusals from three signs: its guard, a { in its path, and whether it takes input. Its docstring sets the bar: "a description that says a collection may answer 404 is a description that lies". It misses refusals raised in the body of a call, and the one raised before the call reads its body. A client generated from openapi.json has no type for those answers, including the two a capturing extension is most likely to meet.

  • Where:
    • api/problems.py:303-322, _statuses: 401 and 429 always, 403 for a scoped guard, 404 only if "{" in path, 422 when the call has models.
    • api/api.py:413-448, create_capture: idempotency.once raises Refused(409, …, kind="idempotency-key-in-use") (api/idempotency.py:99).
    • api/problems.py:183-207, _too_big, the RequestDataTooBig handler: a 413 too-large for any body over DATA_UPLOAD_MAX_MEMORY_SIZE, on every call that takes one. The wiki names a capture whose html reaches about 2.5 MB as the case that meets it (postulo.wiki/The-capture-API.md:189-190).
    • api/routers/interviews.py:111-113, add_interview, and api/routers/reminders.py:42-44, add_reminder: raise HttpError(404, "No such application.") for an application_id in the body that is not the caller's, on paths without {.
  • What happens: the probe of api.get_openapi_schema() gives ['201', '401', '403', '422', '429'] for each of POST /api/v1/captures, POST /api/v1/interviews and POST /api/v1/reminders, with no 409, no 413 and no 404. The PUT /captures/{pk}/rendering call gets its 409/411/413/415 right only because it declares them itself, through openapi_extra=_A_RENDERING (api/api.py:671-679).
  • Why it matters: the browser extension sends the whole page source to POST /captures. When it is too large, the answer is a typed too-large problem carrying max_bytes, but a generated client treats it as an unexpected status and cannot say "send less". The same goes for a retried capture meeting idempotency-key-in-use, which the wiki tells the client to wait out and resend.
  • Fix: have _statuses add 413 to every operation with a request body, and let an operation add statuses of its own. For example, read an attribute set by a small decorator, or merge openapi_extra["responses"] as attach_rendering already does. Then declare 409 on create_capture and 404 on add_interview and add_reminder.

Related to #296 (closed), which introduced the per-call description.

Done when

  • tests/test_api_problems.py has a test asserting that POST /api/v1/captures is described with 409 and 413, and POST /api/v1/interviews and POST /api/v1/reminders with 404. It fails today.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, comparing the raise sites with what `api.get_openapi_schema()` describes for each call. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `api/problems.py:303-320` (`_statuses`, which since #248 is handed the scoped guard rather than importing it), `api/api.py:415-450` (`create_capture`) and `api/api.py:673-681` (the responses `_A_RENDERING` declares); the `_too_big` handler, `api/idempotency.py:99` and the two routers have not moved, and the probe gives the same lists. `problems._statuses` works out each call's refusals from three signs: its guard, a `{` in its path, and whether it takes input. Its docstring sets the bar: "a description that says a collection may answer 404 is a description that lies". It misses refusals raised in the body of a call, and the one raised before the call reads its body. A client generated from `openapi.json` has no type for those answers, including the two a capturing extension is most likely to meet. - **Where:** - `api/problems.py:303-322`, `_statuses`: 401 and 429 always, 403 for a scoped guard, 404 only `if "{" in path`, 422 when the call has models. - `api/api.py:413-448`, `create_capture`: `idempotency.once` raises `Refused(409, …, kind="idempotency-key-in-use")` (`api/idempotency.py:99`). - `api/problems.py:183-207`, `_too_big`, the `RequestDataTooBig` handler: a 413 `too-large` for any body over `DATA_UPLOAD_MAX_MEMORY_SIZE`, on every call that takes one. The wiki names a capture whose `html` reaches about 2.5 MB as the case that meets it (`postulo.wiki/The-capture-API.md:189-190`). - `api/routers/interviews.py:111-113`, `add_interview`, and `api/routers/reminders.py:42-44`, `add_reminder`: `raise HttpError(404, "No such application.")` for an `application_id` in the body that is not the caller's, on paths without `{`. - **What happens:** the probe of `api.get_openapi_schema()` gives `['201', '401', '403', '422', '429']` for each of `POST /api/v1/captures`, `POST /api/v1/interviews` and `POST /api/v1/reminders`, with no 409, no 413 and no 404. The `PUT /captures/{pk}/rendering` call gets its 409/411/413/415 right only because it declares them itself, through `openapi_extra=_A_RENDERING` (`api/api.py:671-679`). - **Why it matters:** the browser extension sends the whole page source to `POST /captures`. When it is too large, the answer is a typed `too-large` problem carrying `max_bytes`, but a generated client treats it as an unexpected status and cannot say "send less". The same goes for a retried capture meeting `idempotency-key-in-use`, which the wiki tells the client to wait out and resend. - **Fix:** have `_statuses` add 413 to every operation with a request body, and let an operation add statuses of its own. For example, read an attribute set by a small decorator, or merge `openapi_extra["responses"]` as `attach_rendering` already does. Then declare 409 on `create_capture` and 404 on `add_interview` and `add_reminder`. Related to #296 (closed), which introduced the per-call description. ## Done when - `tests/test_api_problems.py` has a test asserting that `POST /api/v1/captures` is described with 409 and 413, and `POST /api/v1/interviews` and `POST /api/v1/reminders` with 404. 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#431
No description provided.