Repository navigation
GitHub HTTP response status is lost on truncated body reads, leaving request budget blind #109
Description
Verified adjacent implementation finding (2026-10-30)
Source boundary: release/0.6.0 at 6d935ee111e2efab7a5f7d7c8f8d050af8d35567; src/devostasis/adapters/github.py blob 7a998b94b12547ef543a6320360ed94f088197a7. Related but distinct from #108, which covers completed HTTP responses with malformed JSON; this case concerns a response whose HTTP 200 status and headers have arrived, but whose body read fails. This is not a new Vital, threshold, or policy change.
Reproduction and precise boundary
UrllibTransport.get() obtains a response object in with self._open(request) as response:, then calls response.read() before returning (response.status, headers, body). If response.read() raises http.client.IncompleteRead, the outer except (urllib.error.URLError, http.client.HTTPException, TimeoutError, OSError) raises NetworkFailure without propagating the already observed status or headers. GitHubClient._transport_get() has already incremented request_count, but GitHubClient.get() never reaches its billed_count += 1 path; the next _claim_request() checks only billed_count.
Offline native stdlib loopback proof: a local HTTP server sends 200, Content-Length: 100, and only two body bytes ([]), then closes. Python urllib returns a response with status == 200, while response.read() raises IncompleteRead(2 bytes read, 98 more expected). Repeated twice: 2/2 observed HTTP 200 header responses and 2/2 IncompleteRead, with the server confirming both requests. A separate source-shaped extraction of the transport/client budget control flow with budget=1 performed two dispatches, request_count=2, billed_count=0: a physical request cap was not enforced. Both probes used only the loopback interface and no external GitHub calls.
Minimal reproducer for the native read boundary:
# Inside a local BaseHTTPRequestHandler.do_GET():
self.send_response(200)
self.send_header("Content-Length", "100")
self.end_headers()
self.wfile.write(b"[]")
self.close_connection = True
# Inside the client:
with urllib.request.urlopen(local_url, timeout=2) as response:
assert response.status == 200
response.read() # raises http.client.IncompleteReadImpact / uncertainty
A status-known-but-truncated response is recorded as a generic NetworkFailure, so billing evidence visible before the read failure is discarded. An API provider may charge such a request; the loopback experiment does not prove GitHub's accounting or a real production billing incident. In this path, a configured billed-response budget does not bound the number of physical dispatches across separate get() calls, even when status=200 was observed each time. The collector does not convert the truncated body into a successful exact inventory; this finding concerns liveness, provenance and request accounting, not false AVAILABLE metrics.
Requested repair / acceptance
- Preserve available HTTP status/headers through a truncated-body read failure (or an equivalent typed transport outcome) without accepting or caching partial JSON. Keep
ERROR/explicit incomplete evidence and existing read-only transport behavior. - Add real
UrllibTransport+GitHubClientregressions using a local or fake response that raisesIncompleteReadafter an observed HTTP 200: check counters, subsequent dispatch eligibility, cache integrity, andrun_metaunder a bounded budget. Include an unknown-status network failure control and 304 exemption. - Decide explicitly whether known HTTP status is sufficient evidence for charging
billed_requests, and/or whether an independent physical-attempt cap is needed. Do not silently treat unknown provider billing as zero or change existing budget semantics without an accepted contract/compatibility decision. GitHub transport: malformed completed responses bypass billed-request budget #108's completed-body fix is orthogonal. - Run full offline pytest, sample/historical verification, installed-wheel and exact-head CI before claiming release acceptance.
Evidence boundary: native stdlib loopback behavior and source-shaped Devostasis control-flow reproduction passed; full installed Devostasis, GitHub provider quota behavior, pytest, conformance and hosted CI were NOT_RUN. No repository code change or merge is claimed. Dedup search of open/closed issues/PRs for IncompleteRead and truncated-body billing found no dedicated issue; #108 covers completed JSON decode failures instead.
Activity
andy-zen-dev commentedon Oct 30, 2026
#109 implementation handoff — retain known HTTP status across failed body reads (2026-10-30)
Pinned evidence:
release/0.6.0at6d935ee111e2efab7a5f7d7c8f8d050af8d35567,src/devostasis/adapters/github.pyblob7a998b94b12547ef543a6320360ed94f088197a7;CONTRIBUTING.mdwas consulted. Authenticated accountandy-zen-dev, permissionpull=true,push=false,triage=false,maintain=false,admin=false. No source mutation was possible.Prepared narrow prerequisite fix (NOT integrated): Keep the existing
NetworkFailureexception class and add optionalstatus_code/response_headersmetadata. InUrllibTransport.get(), capture the actual response status and normalized headers before callingresponse.read(); wrap only failures occurring after that point inNetworkFailure(..., status_code=status, response_headers=headers). The failed body is not returned, parsed, or cached. Errors before headers remain status-less. Normal 200/JSON handling and the 304 cache exemption remain unchanged. Existing callers constructingNetworkFailure(message)remain compatible.Key source edits:
Prepared standard unified patch also includes the new
tests/test_github_transport_provenance.py(5 parametrized/standalone test cases): observed 200 withIncompleteReadorTimeoutErrorretains the known status and headers, cannot poison a preexisting ETag cache, a pre-headerURLErrorremains unknown-status, a complete 200 still bills once, and a valid cached 304 still replays without billing. Patch SHA-256:57800cd6fbea02e9776507f785e95840c9be629de82ec2905746183569965734.Verification performed:
git apply --check, patch application, and test-module syntax: PASS on a synthetic reconstruction of the exact GitHub source hunk contexts, not a full checkout. Additional 5/5 isolated smoke scenarios PASS against source-extracted patchedUrllibTransport.getfor truncated read, post-header timeout, pre-header failure, valid 200 and malformed 200. Full real-package pytest, conformance, persisted-bundle verification, installed wheel, exact-head GitHub CI: NOT_RUN. No PR or merge was created.for:researcher — genuine pending contract decision: A response with observed HTTP 200 headers but an incomplete body might have consumed provider quota. Should the
billed_requestsmetric charge an observed non-304 status before body completion, or should such calls remain charge unknown and be bounded separately by a physical-attempt cap? Decide and version the definition, required run-metadata/provenance, and compatibility impact before modifyingGitHubClient.billed_countor_claim_request(). This prerequisite patch intentionally does not fix #109's request-budget escape until the accepted contract specifies charging/attempt limits; it only preserves the evidence needed to implement the correct choice. #108's completed-body accounting is a distinct accepted path.Suggested next implementation unit after decision: handle
NetworkFailure.status_codein the client at the correct exactly-once charging boundary, add the realUrllibTransport + GitHubClientregression at budget=1 asserting second-dispatch behavior andrun_meta, then run the complete repository gates before release admission.andy-zen-dev commentedon Oct 30, 2026
#109 follow-up: combined #105–#109 implementation-stack compatibility, with an explicit remaining budget escape (2026-10-30)
Scope and authority: Independent uncommitted integration check. Re-read
release/0.6.0src/devostasis/adapters/github.pyblob7a998b94b12547ef543a6320360ed94f088197a7,src/devostasis/adapters/cache.pyblob0d4de1e063eb6b0527b5b7b44c7fa0a7e830d085,CONTRIBUTING.md; observed GitHub accountandy-zen-dev,pull=true,push=false,triage=false,maintain=false,admin=false. This is not a release acceptance, new research judgement, merge, or change to upstream.Prepared patches on the same pinned source:
9263eb52f70ec2d3575afed01feb6314f53b223ccb4597beb46ce095ba079569(handoff in GitHub HTTP 202 Accepted can be cached as a completed empty inventory #107).77a2f2904621abfb09f7a35c4e0c5fac6b93b3ef4038b8c75172fba160d27aa8(handoff in GitHub transport: malformed completed responses bypass billed-request budget #108).57800cd6fbea02e9776507f785e95840c9be629de82ec2905746183569965734(previous comment here).New exact-scope verification:
git apply --check, sequential application, andgit diff --checkPASS in all six patch orders. Both added test modules compile. This does not prove applicability in a complete checkout, only the reconstructed contexts.GitHubClient.getalgorithm plus scripted transport: known-status malformed JSON 200/202/206 is charged exactly once and never cached; returned unexpected 202/206/302/307 is rejected and charged; complete 200 and valid cached 304 retain their distinct accounting; status-aware truncated-bodyNetworkFailurepropagates without accepting/caching its body.IncompleteReadnow preserves status metadata, but the client still does not chargebilled_countor cap physical attempts. Withbudget=1, an isolated source-shaped test seesrequest_count=1,billed_count=0after that failure and permits a second physical dispatch (request_count=2,billed_count=1after a subsequent 200). Repeated incomplete reads remain unbounded by the billed-response budget. This was deliberate: thefor:researcherdecision requested earlier in GitHub HTTP response status is lost on truncated body reads, leaving request budget blind #109 is still unanswered. Do not close GitHub HTTP response status is lost on truncated body reads, leaving request budget blind #109 or characterize these tests as a fixed budget escape.Boundary: all synthetic reconstructions/extracted-algorithm checks, not installed Devostasis. Full
pytest, conformance, sample/historical bundles, wheel and exact-head hosted CI areNOT_RUN. No branch/commit/PR/merge. The complete repro scripts are local work artifacts, not GitHub-persisted files.Next actionable unit once research decides charging semantics: apply the three non-overlapping patches to a real fix branch; implement and test #109's budget accounting/physical-attempt limit according to the accepted versioned decision, including real
UrllibTransport + GitHubClientandrun_metaregression at budget=1, then run the full repository gates and obtain independent exact-head review. #95 is separately approved but not merged; #81 remains a release dependency.andy-zen-dev commentedon Oct 30, 2026
Redundancy correction — integration replay of prepared HTTP patch stack (2026-10-30)
This run independently reran the three prepared patch packages on an exact-hunk synthetic reconstruction at
release/0.6.0source blob7a998b94b12547ef543a6320360ed94f088197a7. All six patch application orders passedgit apply --check, application,git diff --check, and syntax compilation of the added pytest modules. Replay script SHA-256:1432edbf36dee91642d28eddcf2526b13743004c7019c255d07afd4046cfb13e. Re-executed separate isolated status-gate and #105–#108 interaction probes (109/109 and 16/16).Correction: the immediately preceding #109 comment #6523660138 already reports the same six-order compatibility test plus a stronger 21/21 interaction matrix and the unresolved physical-budget escape. This replay does not establish a new code fix, research judgement, release gate, or material improvement beyond that handoff. Treat the preceding comment as the authoritative integration evidence and avoid duplicate implementation work.
The current #109 patch preserves an observed HTTP status on truncated body reads but does not enforce a physical-attempt cap or account for its possibly billed status. Research decision is still pending. Full checkout, installed-package pytest, historical bundles, wheel and exact-head CI: NOT_RUN. GitHub identity
andy-zen-devhaspull=true, push=false, triage=false, maintain=false, admin=false; no branch, commit, PR, review verdict, or merge was produced.