Scheduler: webhook deliveries and browser pushes have no overall deadline or size cap, so one receiver can stall a pass #365
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#365
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 shared HTTP client and the two notifiers the scheduler calls inline.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/http.py:139-145(_build) andnotifications/management/commands/send_due_reminders.py:189-198(the pass); the other locations are unchanged.#321 §3 records that the capture and logo fetches buffer the whole body and time out per read only. The shared client every connection uses has the same weakness, and two of its callers run inside the scheduler pass that serves every account. A receiver chosen by one account can hold that pass, and with it everybody's reminders, store copies and syncs, for as long as it keeps sending a byte now and then.
plugins/http.py:137-143(_build:httpx.Client(timeout=timeout, ...), with no streaming, byte limit or overall deadline);plugins/webhook/__init__.py:63(TIMEOUT = 10.0, "How long one delivery may take") and:232-242(post:http.post(...), buffered);plugins/browser/webpush.py:225-240(push:client.post(subscription.endpoint, ...), buffered);notifications/webhooks.py:155-165(send_pending, up toBATCH = 50rows with no time budget);notifications/management/commands/send_due_reminders.py:187-196, where reminders (whose browser pushes run insidenotify), copies, webhooks and syncs run one after another.run_syncshas abudget, but it only decides whether to start the next sync;send_pendinghas none.https://address (parse_subscriptionchecks the scheme only)._build, read responses through a wrapper that streams and stops past a cap (a webhook or push reply needs a few kilobytes; a caller that needs more asks for it) and enforces a deadline on the whole request. Givewebhooks.send_pendingthe same time budget the syncs have, and stop starting deliveries once it is spent.Done when
tests/test_webhooks.pyhas a test with a transport that yields slowly past the deadline and one that yields more than the cap, each expecting the delivery to fail promptly with a recorded reason, and a test thatsend_pendingstops starting deliveries once its budget is spent.Related to #321 §3, which covers the same weakness in
fetching._read_cappedandlogos.downloadonly.Evidence from the review of #321 on 2026-09-30, for the browser-push half.
plugins/browser/webpush.py::push()callsclient.post(subscription.endpoint). That reads the push service's whole answer, withbroffered, andparse_subscriptionchecks only the endpoint's scheme, so it can be anyhttps://address an account holder stores. Measured: a 316-byte brotli answer was held as 200 MB in the worker.#321 adds
http.read_body(response, limit, deadline): a streamed read with a size limit, bounded decompression and a whole-download deadline. It would fit both paths here.http://receiver, so notification text leaves in clear #970