Checking a career link: the fallback GET reads the whole response into memory, with no size cap #619
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#619
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 link check's requests.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/http.py:157-167(public_only_client) andplugins/http.py:139-145(_build);resume/links.pyandresume/views.pyare where they were.Check it answers and Check them all send
HEAD, and when the answer is 401, 403, 405, 501 or any 5xx they ask again withclient.get(url). That is a non-streaming request, sohttpxreads the entire body into memory before returning, and nothing in the public-only client limits its size. Only the status code is used; the body is never needed.resume/links.py:67-73(_ask:if response.status_code in (401, 403, 405, 501) or response.status_code >= 500: response = client.get(url)), with the client fromplugins/http.py:155-165(public_only_client), built by:137-143(_build) with a timeout and no size limit. Reached fromresume/views.py:364-380(LinkCheckView, one link or every link).HEAD(405), or at a pre-signed object-storage address, which is signed forGETand answersHEADwith 403. Pressing Check makes the web worker download and hold the whole file. The 8-secondTIMEOUTapplies to each read, not to the whole download, so a steady stream is never cut off.HEAD.fetching._read_cappedandlogos.downloadbut not this call.with client.stream("GET", url) as response:and read nothing (or at most a few kilobytes) before closing, since only the status is wanted. If #321's fix adds a capped-read helper toplugins/http.py, use it here as well.Done when
tests/test_links_and_letters.pyanswersHEADwith 405 through a mock transport andGETwith a body that never ends, and shows that the check returns the status without reading more than a small bound.Evidence from the review of #321 on 2026-09-30, which measured this and found the same pattern in two more places.
resume/links.pyalso leavesfollow_redirectson, so each redirect's body is read whole too. It sends httpx's defaultAccept-Encoding, which includesbr: a 316-byte brotli answer was held as 200 MB.accounts/avatars.py(the Gravatar fetch:client.get, thenresponse.content) andcore/updates.py::_fetch(client.get, then.json()). Both go to fixed hosts over HTTPS, so they matter less, but they have no cap either.#321 adds
http.read_body(response, limit, deadline)for the capture, logo and robots.txt fetches. It streams, stops at a size limit, bounds decompression, and holds the whole download to a deadline. Using it here, withAccept-Encoding: gzip, deflateandfollow_redirects=False, would close all three.