Skip to content

fix: raise GitHubDocsError for a read timeout or a non-JSON 2xx body - #13

Merged
dmccoystephenson merged 2 commits into
mainfrom
fix/request-read-timeout-and-non-json-body
Sep 27, 2026
Merged

dmccoystephenson merged 2 commits into
mainfrom
fix/request-read-timeout-and-non-json-body

Conversation

@dmccoystephenson

@dmccoystephenson dmccoystephenson commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • GitHubDocsClient._request now converts a 2xx body that fails to decode into GitHubDocsError carrying the response status. The catch is ValueError, which covers both json.JSONDecodeError and the UnicodeDecodeError raised for bytes that are not UTF-8. The body is not quoted back, matching how the error path already handles a non-JSON body.
  • A bare OSError escaping the request (a socket.timeout raised by resp.read(), or a connection reset mid-read; urlopen wraps neither in URLError) is now converted into GitHubDocsError("could not reach GitHub: …") with no .status. OSError was chosen over socket.timeout alone because a reset during the read breaks the README's contract in exactly the same way.
  • Both new messages pass through self._redact(...).
  • Three TestErrorSurface cases were added, one per path.

python/README.md's Errors section ("Everything that stopped an edit from landing raises GitHubDocsError") already stated the intended contract and is now accurate; it is unchanged. One remaining path, a timeout inside the HTTPError handler's own e.read(), is tracked separately in #14.

Half touched: python/ only.

Test plan

  • Full suite locally: Ran 49 tests … OK (run under local Python 3.8 via a sys.path runner, since env-var prefixes were unavailable in the sandbox; CI's 3.9–3.13 matrix is the real anchor).
  • Stash-and-run: with each production change reverted, the tests that cover it ERROR; with it restored, all tests pass (details are in the self-review comment).
  • CI green on every js and python leg.

Merge note

This diff touches error-message construction in python/src/github_docs/client.py, which is on the loop's do-not-auto-merge list (the token-redaction path is a security control). It is therefore left for human review rather than merged autonomously.

Deferred issues

Closes #11

This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).

🤖 Generated with Claude Code


drafted by Claude on behalf of Daniel Stephenson

dmccoystephenson and others added 2 commits September 26, 2026 01:56
_request caught HTTPError and URLError only, so a timeout raised by
resp.read() and a JSONDecodeError on a 2xx body escaped as themselves,
breaking the README's promise that everything which stops an edit
landing is a GitHubDocsError. Both are now converted, through _redact,
with the body never quoted back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
json.loads on bytes that are not UTF-8 raises UnicodeDecodeError, which
is not a JSONDecodeError, so it still escaped. Catch ValueError, which
covers both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Contributor Author

Self-review rubric (head 31d40af):

  • Scope: PASS — only python/src/github_docs/client.py (_request) and python/tests/test_client.py are modified, both required by A read timeout or a non-JSON 2xx body escapes save_file as something other than GitHubDocsError #11.
  • Tests-new: PASS — no new public symbol; three TestErrorSurface cases cover the three new conversion paths (read timeout, non-JSON 2xx, non-UTF-8 2xx).
  • Tests-fix: PASS — stash-and-run on commit 1: with client.py reverted, test_a_timeout_while_reading_the_body_becomes_a_readable_error and test_a_success_body_that_is_not_json_becomes_an_error_with_its_status ERROR; restored, 48/48 OK. Stash-and-run on commit 2: with the ValueError widening reverted, test_a_success_body_that_is_not_utf8_becomes_an_error_with_its_status ERRORs; restored, 49/49 OK.
  • Sibling structure: PASS — new tests follow the TestErrorSurface pattern (mock.patch("urllib.request.urlopen", …), _response helper, assertRaises(GitHubDocsError)).
  • Sibling renames: PASS — no renames.
  • Docs: PASS — python/README.md Errors section already stated the intended contract and is now accurate; no API, config or save-sequence change.
  • Issue resolution: PASS — both paths named in A read timeout or a non-JSON 2xx body escapes save_file as something other than GitHubDocsError #11 are converted and tested.
  • CI: PASS — all js and python legs green on the PR head.
  • No-leak (python): PASS — both new GitHubDocsError messages go through self._redact(...); the 2xx body is never included (asserted by assertNotIn("captive portal", …)).
  • Stdlib-only: PASS — no import added under python/src/; socket is imported only in the tests.
  • Sentinel intact: PASS — TOKEN is unchanged.
  • Export parity / Result-not-exception / No runtime dependency (js) / Support-matrix parity: N/A — js/ and the manifests are untouched.

Findings:

  • The first pass FAILED on an adversarial check: json.loads applied to bytes that are not UTF-8 raises UnicodeDecodeError, not JSONDecodeError, so a 2xx body of that kind still escaped. This was fixed mechanically in 31d40af (except ValueError, which covers both), and a regression test was added.
  • python/src/github_docs/client.py:252 (outside the diff hunk): e.read() inside the HTTPError handler can itself time out. An exception raised inside an except block is not caught by the sibling except OSError, so that path still escapes. This is out of scope for A read timeout or a non-JSON 2xx body escapes save_file as something other than GitHubDocsError #11 and was filed as a separate issue.
  • except OSError is broader than the socket.timeout named in A read timeout or a non-JSON 2xx body escapes save_file as something other than GitHubDocsError #11. This was deliberate, because a connection reset mid-read breaks the same contract. str(e) for socket errors does not carry the request URL, and the message still passes through _redact.
  • Merge hold: the diff touches error-message construction in python/src/github_docs/client.py, which is on the do-not-auto-merge list. The PR is left open for human review.

This review comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

@dmccoystephenson
dmccoystephenson merged commit fcfe7f4 into main Sep 27, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A read timeout or a non-JSON 2xx body escapes save_file as something other than GitHubDocsError

1 participant