API lists: limit has no upper bound, so one call can shape a whole account inside the write lock #434
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#434
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, reading the API's pagination and the SQLite transaction settings.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The list inapi/api.pyis now atapi/api.py:632;api/paging.py, the routers and the settings have not moved.Every paginated API list accepts any
limit:Pageextends django-ninja'sLimitOffsetPagination, whose input islimit: int = Field(100, ge=1), and noNINJA_PAGINATION_MAX_LIMITis set, so ninja's ceiling is infinity. #230 moved row shaping after the cut so that a page's work is one page's work, but?limit=1000000makes the page the whole account. The call runs underATOMIC_REQUESTS, and on SQLite every transaction isIMMEDIATE, so the instance's write lock is held while every row is loaded, shaped and serialised.api/paging.py:45-62(Page, inheriting ninja's unboundedInput);config/settings/base.py:140(ATOMIC_REQUESTS, and noNINJA_PAGINATION_MAX_LIMITanywhere inconfig/);config/sqlite.py:49("transaction_mode": "IMMEDIATE"). The eleven@paginate(Page, ...)lists inapi/api.py:630andapi/routers/all inherit it.api/routers/search.py:17does bound its ownlimit(le=50), so the API is already inconsistent.GET /api/v1/applications?limit=100000, or/listings,/capturesand the rest. Each row is built by itsrowfunction (absolute URLs, the display annotations, the status log) for the whole account in one request, while the scheduler,db_workerand every other person's writes on the instance wait for the lock; past the twenty-second busy timeout they fail with database is locked. The per-token allowance (POSTULO_API_RATE, 600 an hour by default) lets it repeat.config/sqlite.pyset out to prevent, a single request holding the write lock for as long as it likes.NINJA_PAGINATION_MAX_LIMITinconfig/settings/base.py(200, say), or givePageits ownInputwithlimit: int = Field(100, ge=1, le=200)so the bound shows in the OpenAPI schema. State the maximum in the wiki's pagination paragraph of The capture API.Done when
tests/test_api_lists.pythat asksGET /api/v1/applications?limit=10000gets either a 422 or a page of at most the documented maximum, and the OpenAPI schema declares the bound onlimit.Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#434).