Repository navigation
fix(github): fail closed on every non-success response and malformed payload - #7
Merged
Merged
Conversation
…rrors GitHubRateLimitException and GitHubRepositoryNotFoundException now extend a common GitHubClientException base with a failureCode() contract, and gain siblings for authentication/permission, validation/conflict, transient, malformed-response and truncated-tree failures (F-006). Historical codes for the two existing exceptions are preserved via the default Str::snake(class_basename()) fallback; only the new categories get explicit, clean literals. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…esponse request() now maps every GitHub API outcome to an explicit domain exception instead of letting anything besides 403/404 fall through as a normal Response: 401 and 403-without-rate-limit-signal become authentication failures, 429 and 403-with-signal become rate limiting, 409/422 become validation failures, 5xx and connection failures become transient errors, and any other non-2xx/304 status is treated as malformed. markdownFiles() validates the tree payload is actually an array before use and fails closed with a dedicated code when GitHub reports the tree as truncated, instead of silently processing a partial listing. readMarkdownFile() uses strict base64 decoding and fails closed instead of turning invalid content or an unsupported encoding into a silently empty document. The 304/ETag conditional-request path is untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
run() previously caught only GitHubRateLimitException and GitHubRepositoryNotFoundException. Any other GitHub client failure (e.g. the unsupported-encoding case) escaped run() entirely, leaving the IngestionRun stuck in "running" with no finished_at and no log entry. Catching the common GitHubClientException base and using its failureCode() ensures every category introduced for F-006 ends the run explicitly as "failed" with a stable, logged code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tions Adds GitHubClientFailClosedTest with one case per F-006 category (401, 403 with/without rate-limit signal, 429, 409/422, 5xx, connection failure, non-JSON body, missing tree, truncated tree, invalid base64, unsupported encoding), each asserting the concrete exception type and its failureCode(). Also regression-tests that the 304/ETag path still returns unchanged content, and that a transient GitHub failure ends the ingestion run as "failed" with a logged code instead of leaving it stuck running. Replaces the uncommitted GitHubFailClosedReproTest scaffold, which only dumped output and asserted toBeString() without proving any behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Declared but never called; its docblock also claimed an unverified "before" column. Residue from the F-006 fail-closed test authoring. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Record ADR-27 with the failure-code taxonomy and the scope trade-offs, add BRAIN-007 on external integrations failing closed, mark F-006 as mitigated in the system audit, update the canonical state, drop the delivered backlog item, and leave the handoff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LuanTrindade95
added a commit
that referenced
this pull request
Sep 20, 2026
…sanitization Renumbers the sanitization ADR to ADR-28 because main took ADR-26 and ADR-27, keeps both remediation phases in the canonical state, and drops the two backlog items completed by this branch and by PR #7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes remediation item C2 (audit finding F-006).
Problem
HttpGitHubClient::request()only mapped 403 to rate limit and 404 to repository not found. Every other failure — 401, 409, 422, 429, 5xx, connection errors — came back as an ordinaryResponse, andmarkdownFiles()consumed it withjson(''tree'', []). A server error, a non-JSON body or a body withouttreebecame an empty list, so ingestion finishedsuccesswith zero documents, indistinguishable from a repository that legitimately has no docs. Invalid base64 was persisted as an empty document, and atruncated: truetree was treated as complete.A second fail-open path, not described in F-006, is closed here too:
IngestionService::run()only caught the two existing exceptions, so any other one escaped the method and left theIngestionRunstuck inrunning, with nofinished_atand no log.Change
App\Exceptions\GitHubClientExceptionis the abstract base for every GitHub failure and exposesfailureCode(). The client now fails closed on every non-success status and validates the payload;run()catches the whole hierarchy and ends the run throughfail().git_hub_rate_limit_exceptiongithub_authentication_failedgit_hub_repository_not_found_exceptiongithub_validation_failedgithub_transient_error>= 500, connection, timeoutgithub_malformed_responsetree, invalid base64, unsupported encodinggithub_tree_truncatedtruncated: trueA 403 is classified by the rate-limit header rather than the status alone, so a permission denial is no longer misreported as rate limiting. Transient matches a range, not a list, so unseen 5xx stay covered.
Compatibility
App\Contracts\GitHubClient,FixtureGitHubClient, theIngestionService::fail()signature and theIngestionRun.logentry shape are all unchanged — downstream consumers read the taxonomy through the existingcodekey with no migration. The two legacy codes keep their historical shape because a test asserts one literally. The 304/ETag path of ADR-11 returns before any error check and is covered by a regression test. No retry or backoff: that remains Priority 8.Validation
Http::fakeharness drivingIngestionService::run()end to end, reading status and log from the database. No error scenario finishedsuccess; no two distinct categories shared a code.IngestionRun.log,storage/logs, exception messages and the diff: no occurrence of the token,BearerorAuthorization.skip, no weakened assertion.Worth knowing for future work: the
backendservice has no bind mount, so a gate run straight after an edit can execute stale image code. Rebuild before trusting it.Taxonomy and scope trade-offs recorded in ADR-27; the general rule in BRAIN-007.
🤖 Generated with Claude Code