P2-1: HTTP client core — auth, base URL, error mapping #21
Labels
No labels
data-integrity
engine
platform
procurement
remote
scaffold
ui
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/cairn-desktop#21
Loading…
Add table
Add a link
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?
First task of phase 2. Depends on the
phase-1-enginemilestone being complete.Goal
The plumbing every WebDAV verb sits on: base URL joining, authentication, timeouts, and turning HTTP status codes into typed Go errors the engine and UI can act on.
Files
internal/remote/client.go,internal/remote/errors.go,internal/remote/client_test.goProduces
Typed errors — the engine branches on these
507 matters most. Cairn returns it when a quota is exhausted. It must surface as a clear, actionable state in the UI — never a retry loop, which would hammer the server forever with no chance of success.
401 matters second. It means the app password was revoked; the client must stop and ask for re-authentication rather than retrying.
Steps
httptest.NewServer:#,+, non-ASCII)errors.Is)url.PathEscapeper segment, not on the whole path — escaping/would break it.git commit -s -m "feat(remote): HTTP client core with typed errors"Acceptance criteria
Done
3ab6299d6a— feat(remote): HTTP client core with typed errorsb8a1dddd68— fix(remote): bound Client.Timeout to dial/TLS/headers, not the body; never leak BaseURL credentialsWhat was built
internal/remote/client.go+errors.go:Config,Client,New, unexporteddo, the six sentinel errors,StatusError, per-segmenturl.PathEscapejoin inresolve.errors.Is(err, ErrNotFound)anderrors.Is(err, fs.ErrNotExist)(P2-R3).Timeoutnow bounds only dial/TLS/response-header wait (cloned*http.Transport), not the body read;Newrejects aBaseURLwith userinfo and bothString()methods redact viaurl.URL.Redacted.Tests
client_test.go: sentinel-error mapping (401/403/404/409/423/507) againsthttptest.Serverwitherrors.Is; base-URL join + percent-encoding (space,#,+, non-ASCII) verified against both decodedr.URL.Pathand rawr.RequestURI; Basic-auth header; unmapped 500; credential-redaction tests; two new timeout tests (header stall vs. a slow steady body longer thanTimeout).internal/remote92.1% coverage, package total 91.2%.Acceptance criteria
TestDoMapsStatusToSentinelErrors, all six statuses.TestDoJoinsBaseURLAndPercentEncodesSegments.Config.String/Client.Stringredact password and anyBaseURLuserinfo; review finding F2 (aBaseURL-embedded password leaking through both Stringers andNew's parse error) fixed inb8a1dddwith dedicated regression tests.Rulings
go.modstaysgo 1.25,go mod tidybyte-identical.Client.Get/Delete/Moveon a missing path must satisfy botherrors.Is(err, ErrNotFound)anderrors.Is(err, fs.ErrNotExist)— done once insentinelForStatus.internal/remote;internal/syncnever importsnet/net/http— out of scope for this issue,StatusError.Statusleft exposed for later.Config.BaseURL); Cairn's default per-user home is<server>/dav/home/.adc10aa+client_test.goalone (build failed on undefinedClient/New/Config).go vet/gofmt/go mod tidy/whole-suite re-verified independently on host, windows/amd64 and linux/arm64.do()sends Basic auth pre-emptively and maps a challenge-less 401 by status alone; runtime confirmation deferred to #29.Deferred
task-21-review.md) deferred to the final review: error-body draining/comment, stricterBaseURLvalidation and the string-rebuild inresolve, redirect policy, test-gap hardening (exact non-ASCII/percent-literal encoding), and making the bareErrNotFoundsentinel itself satisfyfs.ErrNotExist.Implemented and reviewed by Claude (subagent-driven), landed on main after review and green CI.