Repository navigation
Conversation
claudear's local HTTP client is moving to abnegate-http 0.1.2. Its consumers call the crate's HttpClient and HttpResponse::json inside functions that return claudear's Result, so the crate error needs one explicit conversion, and that conversion decides which failures the watcher retries (retry_trigger_error_is_transient treats Http and Network as transient). The mapping keeps every classification the local client had: - Request and UnreadableBody become Error::Http, as reqwest errors did, so they stay transient. The crate strips the URL from these errors, so messages no longer repeat a query string. - TooManyRedirects becomes Error::Network, transient like the reqwest redirect error it replaces. - Everything else becomes Error::Other with the crate's message: Json keeps the "JSON parse error: ..." text the local HttpResponse::json produced, and Unsupported, OversizedBody and the refused-destination variants cannot change on a retry. Mapping Json into Http would have made a malformed body look transient and retried it. The last arm also covers variants a later release adds, since the crate enum is non_exhaustive. Tests pin each variant's mapping in error.rs, using a real refused connection (through a client without a proxy) for the transport variants, and the watcher tests pin that a JSON error and an oversized body do not give a retry back while a refused connection does. SentryHttpClient lives in claudear-core's types.rs (also edited by #163) and answers with an HttpResponse, so its return type moves to the crate's HttpResponse here, with its two implementers (source/sentry.rs and the regression/sentry.rs test double); otherwise this commit would not build. The crate's HttpResponse is non_exhaustive, so they build it with HttpResponse::new. #147 and #45 also edit source/sentry.rs. The lockfile gains abnegate-http and dashmap 6. abnegate-http needs tokio 1.53, so tokio moves 1.50.0 -> 1.53.2 with tokio-macros pinned at 2.7.0 (as #168 does; 2.7.2 would pull in syn 3), and mio, socket2 and once_cell take patch bumps. #163, #167, #168 and the dependabot PRs also touch Cargo.toml and Cargo.lock. The sentry.rs boundary test loses its narrating comments and the file its section header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The integrations crate's GitHub, GitLab, HelpScout, Discord, notifier and source clients used claudear-core's local HttpClient, HttpResponse and ReqwestHttpClient, which abnegate-http 0.1.2 now provides. - Imports point at abnegate_http. The crate's HttpResponse is non_exhaustive, so every response is built with HttpResponse::new. - The HttpClient test doubles (github.rs, gitlab.rs, source/github.rs, source/gitlab.rs, source/helpscout.rs) answer with abnegate_http::Result, the trait's own result type. Production code converts with `?` through the one From impl in claudear-core, so JSON errors keep their variant and message. - The crate's ReqwestHttpClient::new returns a Result and the crate has no Default. GitHubClient, GitLabClient and HelpScoutSource keep their infallible constructors: they fall back to reqwest's default client when the configured one fails to build, exactly as the local ReqwestHttpClient::new did. The trusted transport is kept on purpose; PublicClient would refuse self-hosted GitLab, Sentry and Jira on private addresses. - The crate caps response bodies at 8 MiB, where the local client read them whole. GitHubClient and GitLabClient fetch PR diffs, so their transport reads up to scm::BODY_LIMIT, eight times the crate default (64 MiB). A body over the cap is now an error (Error::Other, not retried) rather than a success. Behaviour of requests that go through ReqwestHttpClient (GitHub, GitLab, HelpScout) changes in ways the crate brings: - A body that cannot be read to the end is an error (Error::Http, retried) rather than an empty successful body. - Bodies decode as UTF-8 with invalid sequences replaced, where reqwest honoured a charset from Content-Type; every API involved sends UTF-8. - Transport errors no longer carry the request URL. - Timeouts (30s, 10s connect), redirects (reqwest's 10 hops), proxy handling and the absent user agent are unchanged. The Discord, Slack, SMS, Telegram, WhatsApp, Jira, Linear and Sentry clients build HttpResponse from their own reqwest calls, so they keep their previous body handling. Tests pin that GitHubClient::new and GitLabClient::new configure their transport with BODY_LIMIT, and drive a transport with that limit against a loopback server: an 8 MiB + 1 byte diff is read whole, a declared length over BODY_LIMIT is refused, and a body cut short is an Error::Http. The loopback transport is built with no_proxy so an HTTP_PROXY in the environment cannot reroute the requests. Touched files also lose their section header comments and the abbreviated names around the changed lines (k/v in the GitHub and GitLab doubles, resp/tok/r/e in HelpScout's source, resp_body in WhatsApp, now text so it no longer shadows the body parameter). notifier/telegram.rs keeps its headers and changes only in its imports and HttpResponse construction, because the Telegram escaping fix stacks on this branch. Overlaps: #147 (source/sentry.rs, via the first commit), #162 (notifier/discord.rs) and #45 (discord/client.rs, notifiers, source/{jira,linear,sentry}.rs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deploy QA tracker, the Linear regression checker and the release
client and tracker used claudear-core's local HTTP client, which
abnegate-http 0.1.2 now provides.
- Imports point at abnegate_http, responses are built with
HttpResponse::new (the crate struct is non_exhaustive), and the test
doubles answer with abnegate_http::Result. Production code converts
through claudear-core's From impl with `?`.
- ReleaseClient::new and LinearRegressionChecker::new stay infallible
and fall back to reqwest's default client when the configured one
fails to build, as the local ReqwestHttpClient::new did; claudear-
analysis now names reqwest directly for that. The already fallible
LinearRegressionChecker::with_embeddings builds its client first and
returns the build error before spending an embedding call.
- The regression/linear.rs MockErrorHttpClient used to fail with
Error::network("connection refused"). The crate has no convenience
constructor for that, so the double makes a real refused connection
to 127.0.0.1:1, through a client it builds once without a proxy, and
converts it into abnegate_http::Error::Request, which maps to the
same transient Error::Http.
- The release tracker imports HttpClient and ReqwestHttpClient instead
of spelling the paths out, its test double indexes responses with
get() instead of a bounds check, and both files lose their section
header banners.
ReleaseClient (used by the release and deploy QA trackers) reads bodies
up to release::BODY_LIMIT, eight times the crate default (64 MiB):
is_commit_in_release fetches compare/{commit}...{tag}, which carries up
to 250 commits and 300 files with patches. Under the 8 MiB default an
oversized comparison would be a non-retried Error::Other and fail the
release watch on every poll. scm::BODY_LIMIT in claudear-integrations
now re-exports this constant, so the GitHub, GitLab and release clients
share one value. A test pins that ReleaseClient::new applies it.
The Linear regression checker keeps the 8 MiB default: its responses
are Linear GraphQL pages and GitHub issue searches.
These clients now error on a body cut short instead of returning it
empty, decode bodies as lossy UTF-8, and drop the URL from transport
errors. Timeouts and redirects are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing imports claudear_core::http any more: every consumer uses abnegate-http 0.1.2's HttpClient, HttpResponse and ReqwestHttpClient, and claudear-core converts the crate's errors in one From impl. The module, its claudear_core::http path and the claudear::http re-export in src/lib.rs go, so there is one HTTP client implementation to maintain. Its HttpResponse tests live on in the crate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🟡 Tier B · Needs changes before merging
Replaces claudear’s local HTTP client with abnegate-http across core, integrations, and analysis, including explicit application error mapping and shared response-body limits for SCM and release requests. Updates the HTTP consumers, test doubles, dependency configuration, and retry-classification tests while removing the former HTTP module and re-exports. Latest changes: The follow-up replaces Debug-based body-limit assertions with loopback behavior tests, moves the shared loopback helpers into analysis behind a test-support feature, and routes the GitHub, GitLab, and release clients through private transport constructors that apply their limits.
Fix with agent prompt### Issue 1
Cargo.toml:154
**Update the Docker toolchain before adding this dependency**
abnegate-http 0.1.2 requires Rust 1.97, but Dockerfile:2 still pins Rust 1.93, so the default image build fails at Cargo's rust-version check. Please land the toolchain update first or include it here rather than merging a dependency that the deployment build cannot compile.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.📂 Walkthrough · 6
⏳ Still open from earlier reviews · 1
Reviewed the commits since |
| http-body-util = "0.1" | ||
|
|
||
| # Shared infrastructure | ||
| abnegate-http = "0.1.2" |
There was a problem hiding this comment.
Update the Docker toolchain before adding this dependency
abnegate-http 0.1.2 requires Rust 1.97, but Dockerfile:2 still pins Rust 1.93, so the default image build fails at Cargo's rust-version check. Please land the toolchain update first or include it here rather than merging a dependency that the deployment build cannot compile.
Prompt To Fix With AI
This is a comment left during a code review.
Path: Cargo.toml
Line: 154
Comment:
**Update the Docker toolchain before adding this dependency**
abnegate-http 0.1.2 requires Rust 1.97, but Dockerfile:2 still pins Rust 1.93, so the default image build fails at Cargo's rust-version check. Please land the toolchain update first or include it here rather than merging a dependency that the deployment build cannot compile.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.🟠 Major · bug · Reply if this doesn't apply.
There was a problem hiding this comment.
Agreed, the image cannot build this until the toolchain moves. That change lives in #161, which sets RUST_VERSION=1.98 in the Dockerfile (abnegate-http 0.1.2 needs 1.97). This PR depends on #161 and must merge after it; the body now says "Depends on #161 (Docker toolchain)" at the top and keeps the build-break note. I kept the toolchain bump out of this PR so every crate migration shares one Dockerfile change rather than each carrying its own.
There was a problem hiding this comment.
This is still an outstanding build break in this PR’s current tree: the Dockerfile pins Rust 1.93 and uses that version for the builder (Dockerfile, builder stage), while the new dependency is declared here (Cargo.toml). Calling #161 a dependency documents the required merge order, but does not make this tree buildable. Please keep this blocked until #161 lands and this PR is based on the toolchain update, or include that update here; I’ll leave the finding open until then.
…g output
The constructor tests read `body_limit: …` out of ReqwestHttpClient's
derived Debug, which the crate can reformat without changing behaviour.
Each client's `new` now hands its transport to a private `with_transport`
that applies BODY_LIMIT, and the tests drive that path against a loopback
server: a body over the crate default is read, and a declared length over
BODY_LIMIT is refused as OversizedBody { limit: BODY_LIMIT }. The tests
inject only the reqwest client, built without a proxy, so an environment
proxy cannot reroute loopback.
The loopback helpers move to claudear-analysis behind a `test-support`
feature so the release client's tests can share them with integrations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Depends on #161 (Docker toolchain).
Build and deploy breaks
rust-version = "1.97", and the Dockerfile pinsRUST_VERSION=1.93. CI usesdtolnay/rust-toolchain@stableand is unaffected.claudear::http,claudear_core::httpand theclaudear_integrations::github::{HttpClient, ReqwestHttpClient}re-export are gone. Nothing in the workspace used them after this change. Anything outside the workspace that imported them should importabnegate_httpinstead.No env var or config key changes.
What changed
claudear-core's local HTTP client (
crates/claudear-core/src/http.rs, 242 lines) is replaced by abnegate-http 0.1.2. The client is deleted, and its 22 consumers useabnegate_http::{HttpClient, HttpResponse, ReqwestHttpClient}. The migration plan split this into three steps (CLA1: core, CLA2: integrations, CLA3: analysis). They ship as this one PR because deleting the local client breaks all 22 importers at once, so separate PRs offmainwould not build. Each of the five commits builds on its own.refactor(core): map abnegate-http errors into the app errorabnegate-http = "0.1.2"as a workspace dependency.From<abnegate_http::Error> for claudear_core::Errorinerror.rs: three arms, with a test for each crate variant (see the table below).retry_trigger_error_is_transient: a JSON error and an oversized body do not give the retry back, and a real refused connection does.SentryHttpClient(intypes.rs, which feat(analysis): so similar-issue suggestions reuse published trial memory #163 also edits) to the crate'sHttpResponse. The edit is two lines. Its two implementers move in the same commit so the commit builds:source/sentry.rs, and theregression/sentry.rstest double.tokio-macrosat 2.7.0, as refactor(analysis): discover dependencies through abnegate-vcs #168 does, so the lockfile gains nosyn3.refactor(integrations): move the HTTP consumers onto abnegate-httpgithub.rs,gitlab.rs,deploy_qa.rs,discord/{client,thread_manager}.rs,notifier/{discord,slack,sms,telegram,whatsapp}.rsandsource/{github,gitlab,helpscout,jira,linear}.rs.HttpResponse::new, because the crate struct isnon_exhaustive.HttpClienttest doubles now answer withabnegate_http::Result.scm::BODY_LIMIT(64 MiB).refactor(analysis): move the HTTP consumers onto abnegate-httpdeploy_qa/tracker.rs,regression/{linear,sentry}.rsandrelease/{github,tracker}.rs.reqwestdirectly, for the constructor fallback described below.MockErrorHttpClientfails with a real refused connection (127.0.0.1:1), converted intoabnegate_http::Error::Request. It makes the connection through a client it builds once, without a proxy.release::BODY_LIMIT = 8 * ReqwestHttpClient::DEFAULT_BODY_LIMIT(64 MiB).ReleaseClient::newapplies it.scm::BODY_LIMITin claudear-integrations re-exports it, so the GitHub, GitLab and release clients share one value. claudear-analysis cannot depend on claudear-integrations, but integrations already depends on analysis.refactor(core): delete the local http clienthttp.rs,pub mod httpand thesrc/lib.rsre-export.test(http): pin the body limits by what the clients read, not by debug output(review follow-up)GitHubClient::new,GitLabClient::newandReleaseClient::newbuild the default transport and hand it to a privatewith_transport, which appliesBODY_LIMIT. Production behaviour is unchanged.body_limit: …out ofReqwestHttpClient's derivedDebugare gone. Each client's limit is now tested by what it reads from a loopback server (see "New tests").serve_once,ok_response,loopback_transport) move fromclaudear-integrations/src/test_support.rstoclaudear-analysis/src/test_support.rs, compiled under#[cfg(any(test, feature = "test-support"))]. claudear-integrations turns the feature on for its dev-dependency only, so the release client and the SCM clients share one server.loopback_transport()builds the reqwest client withno_proxy()and leaves the crate's default limit, so the limit under test comes fromwith_transport.Error mapping (
claudear-core/src/error.rs, one place)The impl has three arms:
Request | UnreadableBody→Http,TooManyRedirects→Network, and everything else →Other(error.to_string()). The tests pin each row:abnegate_http::Errorclaudear_core::ErrorRequest(transport)Http(reqwest::Error)Http, sameUnreadableBodyHttp(reqwest::Error)TooManyRedirectsNetworkHttp(onlyPublicClientraises this variant; the trusted transport still reports redirect loops asRequest)JsonOther("JSON parse error: …")Other("JSON parse error: …"), sameUnsupported(method)Other("POST is not supported by this HTTP client")Other("POST not supported by this HTTP client")OversizedBodyOtherInvalidUrl,InvalidHeaderName,InvalidHeaderValue,UnsupportedScheme,EmbeddedCredentials,MissingHost,PrivateAddress,InternalHost,UnfetchableResolutionOtherPublicClientURL checks, which claudear does not useOthernon_exhaustive)Jsonmaps toOtherinstead ofError::Jsonso the variant and message stay exactly what the localHttpResponse::jsonproduced. A blanket mapping intoHttpwould have made a malformed body look transient.Constructing the client
The crate's
ReqwestHttpClient::new()returns aResult, and the crate has noDefault.GitHubClient::new,GitLabClient::new,HelpScoutSource::new,ReleaseClient::newandLinearRegressionChecker::newstay infallible. When the configured client fails to build, they fall back toReqwestHttpClient::from(reqwest::Client::new()). The localReqwestHttpClient::newdid exactly the same.LinearRegressionChecker::with_embeddingsalready returned aResult, so it now builds the client first and propagates the build error before it spends an embedding call.PublicClient, because self-hosted GitLab, Sentry and Jira run on private addresses.Behaviour differences
These apply to requests that go through the crate's
ReqwestHttpClient:The Discord, Slack, SMS, Telegram, WhatsApp, Jira, Linear-source and Sentry clients make their own reqwest calls. They only build the crate's
HttpResponse, so their transport behaviour does not change.DEFAULT_BODY_LIMIT).BODY_LIMIT, which is 64 MiB (eight times the crate default).ReleaseClientalso reads up toBODY_LIMIT. It is shared by the release tracker and the deploy QA tracker.is_commit_in_releasefetchescompare/{commit}...{tag}, which carries up to 250 commits and 300 files with patches.Error::Other, and the release watch would fail on every poll.Error::Other, not retried) instead of a success.get_pr_differrors on a diff over 64 MiB, and the watcher logs "Failed to fetch PR diff for analysis" at debug level and skips the diff analysis.Error::Http.Httperror.merge_pr,close_pr,delete_branch,close_issueand their GitLab counterparts, after a response whose body breaks off..text()honoured a charset fromContent-Type. Every API involved sends UTF-8.syn3.multipartandqueryfeatures workspace-wide.Crate gaps (follow-ups in abnegate/crates, no shim added here)
Defaultand no infallible constructor forReqwestHttpClient. Five constructors repeatReqwestHttpClient::new().unwrap_or_else(|_| ReqwestHttpClient::from(reqwest::Client::new())). A crateDefaultwould collapse each one toReqwestHttpClient::default().Requesterror is constructible: theregression/linear.rsMockErrorHttpClient(which used to returnError::network("connection refused")) now makes a real refused connection to127.0.0.1:1and converts the reqwest error withabnegate_http::Error::from, which maps to the same retriedError::Http. That costs a loopback connect per call. A constructor such asError::transport(message)would let a test double fail without touching the network.How it was verified
Review follow-up (
aad70a3, on top of the four commits below; no rebase was needed,mainis still 95fab34).claudear-verify.sh <worktree> http readypassed:cargo fmt --all -- --check,cargo clippy --all-targets -- -D warnings -A clippy::double_must_use, andcargo test -p claudear-core -p claudear-config -p claudear-storage -p claudear-analysis -p claudear-integrations -p claudear-engine -p claudear(default features, which include sqlite): 10,473 passed, 0 failed, 3 ignored. The red runs for the new limit tests are described under "New tests". This machine's Homebrew now ships OpenSSL 4, which llama.cpp's vendored cpp-httplib does not compile against, so these local runs setCMAKE_DISABLE_FIND_PACKAGE_OpenSSL=TRUEfor the llama-cpp-sys-2 build. That only drops HTTPS from llama.cpp's own downloader, which claudear does not use; CI is unaffected.The original verification of the four migration commits:
All commands ran in the worktree with
CARGO_BUILD_BUILD_DIR=/Users/jakebarnby/.cargo/build/shared/claudear-migrate CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0, default features plussqlitefor tests. The fastembed model came from a local cache copy, and vectorlite from/opt/homebrew/lib/vectorlite.dylib.cargo check --workspace --all-targetspasses on each of the four commits on its own.cargo fmt --all -- --checkpasses.cargo clippy --workspace --all-targets -- -D warnings -A clippy::double_must_usepasses.cargo test -p claudear-core -p claudear-integrations -p claudear-analysis -p claudear-engine -p claudear --features sqlitepasses (exit 0).tests/e2e_real_repo.rs2,tests/retries.rs1,tests/shutdown.rs13, claudear-analysis 2,885, claudear-core 452, claudear-engine 1,367, claudear-integrations 4,305, other targets 2.no_proxy(), so anHTTP_PROXY/HTTPS_PROXYin the environment cannot reroute it. I re-ran those tests withHTTP_PROXY,HTTPS_PROXYandALL_PROXY(upper and lower case) pointing at a dead proxy on127.0.0.1:9. All 142 selected tests passed: core 7, engine 3, integrations 7, analysis 125 (theregression::linearmodule, including everyMockErrorHttpClienttest).New tests:
claudear-coreerror.rs: one test per crate variant, including that a JSON error keeps the "JSON parse error: " text. The transport variants use a real refused connection.claudear-enginewatcher.rs:retry_trigger_error_is_transientis false for a JSON error and for an oversized body, and true for a real refused connection.github.rs,gitlab.rsandrelease/github.rs: each client is built through the samewith_transportthatnew()uses, with a loopback-only reqwest client, and reads from a loopback server.BODY_LIMIT+ 1 is refused asOversizedBody { limit: BODY_LIMIT }.github.rsalso checks that a body cut short becomesError::Http.with_transporttemporarily applyingBODY_LIMIT / 8(the crate default, as if the limit were removed), all six limit tests failed: the reads over 8 MiB were refused asOversizedBody { limit: 8388608 }, and the refusals reported the wrong limit. With it applyingBODY_LIMIT * 2(a raised limit), the three refusal tests failed, because the declared length was accepted and the read then failed as unreadable instead. Both mutations were reverted before the commit.Clean-ups in touched files
// ---,// ===,// ═══banners) are removed from every file this PR touches, with two exceptions:crates/claudear-core/src/types.rsbelongs to feat(analysis): so similar-issue suggestions reuse published trial memory #163; this PR keeps to its two-line edit there.crates/claudear-integrations/src/notifier/telegram.rskeeps its headers because a follow-up PR (Telegram HTML escaping) stacks on this branch and edits that file.k/vin the GitHub and GitLab doubles;tok/resp/r/ethroughout HelpScout's source (feat(mcp): fuzzy channel names, HelpScout status filter, result-cap notes #166 has merged, so nothing collides);resp_bodyin WhatsApp, nowtextso it no longer shadows thebodyparameter;idxandrin the test doubles.Not verified
--all-features(CUDA) was not built locally.Overlapping open PRs
From
gh pr view <n> --repo appwrite/claudear --json files, intersected with this PR's files:Cargo.toml,Cargo.lock,crates/claudear-analysis/Cargo.toml,crates/claudear-core/Cargo.toml,crates/claudear-core/src/types.rs(two-lineSentryHttpClientreturn-type edit).Cargo.toml,Cargo.lock,crates/claudear-analysis/Cargo.toml,crates/claudear-engine/src/watcher.rs. Both PRs move tokio to 1.53.2 with tokio-macros 2.7.0.Cargo.toml,Cargo.lock,src/lib.rs.crates/claudear-engine/src/watcher.rs.crates/claudear-engine/src/watcher.rs,crates/claudear-integrations/src/notifier/discord.rs.crates/claudear-integrations/src/source/sentry.rs.Cargo.toml,Cargo.lock,crates/claudear-core/Cargo.toml,crates/claudear-core/src/lib.rs,crates/claudear-core/src/http.rs(deleted here);crates/claudear-engine/Cargo.toml,crates/claudear-engine/src/watcher.rs;crates/claudear-integrations/src/discord/client.rs,notifier/{discord,slack,sms,telegram,whatsapp}.rs,source/{jira,linear,sentry}.rs;src/lib.rs.Cargo.lock.Cargo.tomlandCargo.lock.notifier/telegram.rschanges here only in its imports andHttpResponseconstruction.Rebased on #154, #160 and #166.
🤖 Generated with Claude Code