refactor: cleanup sweep across the SDK #82
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!82
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "cleanup/sweep-2026-08"
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?
An extensive cleanup sweep: trim redundant code, remove dead weight, and fix the shortcomings that turned up along the way. 44 files, -323 net lines in
src/(-231 counting the new tests), all 1167 unit tests pass.The bug worth reviewing first
XRPC array query parameters were sent comma-joined. 21 call sites built
?dids=a,bwhere XRPC specifies repeated keys (?dids=a&dids=b), so any endpoint taking an array silently received a single malformed element.AdminClient.GetAccountInfosAsyncwas the giveaway — it built the correct repeated-key list and then threw it away in favour ofstring.Join(",", dids).Affected:
feed.getPosts,feed.getFeedGenerators,actor.getProfiles,labeler.getServices,graph.getRelationships,graph.getStarterPacks,admin.getAccountInfos,label.queryLabels(two params),convo.getConvoForMembers/getConvoAvailability,ozone.signature.searchAccounts, and the eight array filters onozone.moderation.queryEvents/querySubjects. Callers passing one element were fine; callers passing several were querying one bogus identifier.How the fix and the cleanup are the same change
Lexicon clients hand-rolled a
Dictionary<string, string?>at 78 call sites, complete withlimit?.ToString()andincludePins?.ToString()?.ToLowerInvariant(). A dictionary structurally cannot hold repeated keys, which is why the arrays got joined. They now use a new internalXrpcParamsbuilder that drops nulls, formats invariantly, and offersAddAllfor arrays.Other deduplication:
XrpcClient's eight near-identical query/procedure overloads (public + proxied) delegate to oneSendAsyncpair. Declared-type JSON serialization is preserved via a content factory, since the DPoP/429 retry path replays the request andHttpContentcannot be sent twice.RecordCollection'sGet/GetFrom,List/ListFrom,Enumerate/EnumerateFrom— the*Fromvariant is now the implementation.AtProtoClient: four identical delete-by-AT-URI bodies → one helper; two relay-URL guards → one; three hand-copiednew Session { … }blocks → newSession.With(...).Replacehelper.Dead code removed:
XrpcQueryBuilder.BuildQueryString(unused outside its own tests, duplicatingXrpcClient.BuildUrl), itsToDictionaryalias,ModerationClient.AddListParams,Cid's never-calledGeneratedRegex, and anILoggerfield threaded through 21 Lexicon clients that none of them ever wrote to.Other fixes found along the way
RecordCollection.ListAsyncsuppressed failed deserialization withvalue!, producing aRecordView<T>that violated its ownrequired T Valuecontract and threwNullReferenceExceptionsomewhere downstream. Now throws where the failure is, naming the record URI.AtProtoClientFactoryleaked an ECDSA native handle on every failed client build — theAtProtoClientand DPoP session were constructed beforeApplyOAuthSessionAsynctook ownership, and nothing disposed them if it threw.FileAtProtoTokenStorewrote token files in place (a crash mid-write loses the refresh token) and deleted them outside the lock guarding writes (a logout racing a rotation leaves the file behind). Now writes temp-then-move under one lock, and creates files owner-only on Unix.AtProtoHttpExceptiondiscarded the response body it had already read, exactly when the message was least useful. The parse failure was also caught with a barecatch (Exception).AtProtoClientFactoryclaiming the XRPC client refreshes onExpiredToken. It does not, anywhere; per-request clients silently never refreshed.API surface
No signature changed. Two additive members:
Session.With(...)and aninitaccessor onAtProtoHttpException.ResponseBody. The 21 de-loggered constructors areinternal.Tests
XrpcParamsgets its own suite (ordering, null-dropping, invariant formatting,AddAll),XrpcQueryBuilder's tests are retargeted onto the surviving entry point plus a culture-sensitivity case, andLabelerClientTestsnow asserts the repeated-key encoding on the wire so the array bug cannot come back.🤖 Generated with Claude Code
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
This is a well-executed, carefully-scoped cleanup sweep. I traced the core claim — that hand-rolled
Dictionary<string,string?>construction withstring.Join(",", ...)was silently mangling XRPC array parameters — and confirmed the fix is real and consistently applied: every listed call site now uses the newXrpcParams/AddAllbuilder, which emits repeated keys as XRPC requires, andLabelerClientTestsnow pins the wire format so this can't silently regress.The deduplication is sound:
XrpcClient'sSendAsync/SendCoreAsynccollapse the eight overloads correctly, and theFunc<HttpContent>factory is necessary (not just tidiness) becauseSendWithDPoPRetryAsynccan replay the request on a 401/429 and a consumedHttpContentcan't be resent.EnsureSuccessAsync's narrowedcatch (Exception ex) when (ex is JsonException or NotSupportedException)is a genuine improvement over the previous bare catch, andResponseBodyis now preserved on the fallback exception path.RecordCollection.GetAsync/ListAsync/ToViewcorrectly throw instead of the oldvalue!null-suppression, matching whatGetAsyncalready did.FileAtProtoTokenStore's temp-file-then-move write and taking the same_lockinRemoveAsynccorrectly close the crash/race windows described.AtProtoClientFactory's new try/catch disposesoauthSessionandclienton a failedApplyOAuthSessionAsync; I checked whether this double-disposes_oauthSessionwhen the failure happens afterApplyOAuthSessionAsynchas already assigned_oauthSession = oauthSession(before_sessionStore.SaveAsync) —client.Dispose()would then also call_oauthSession?.Dispose(). This isn't a bug:Dispose()onECDsa/OAuthSessionResultis expected to be idempotent per standard .NET convention, and the explicitoauthSession.Dispose()is needed to cover the case where the throw happens before that assignment (e.g. in_xrpc.SetBaseUrl), where the client never took ownership.Replacededup,Session.With, and theDeleteByUriAsynchelper are straightforward and preserve prior behavior.Only a trivial inconsistency found (noted below), not worth blocking on. Nothing in the diff looks like a correctness or security regression, and the changelog entries accurately describe the behavior changes.
⏱ 1m 49s · 19 turns · tokens: 98.0k in / 7.4k out (+2011.0k cache-read)
📜 Full session transcript
@ -205,3 +201,1 @@["includeUsed"] = includeUsed?.ToString().ToLowerInvariant(),["createAvailable"] = createAvailable?.ToString().ToLowerInvariant(),};var parameters = new XrpcParams()GetAccountInviteCodesAsyncstill does.Add("includeUsed", includeUsed?.ToString().ToLowerInvariant())/ same forcreateAvailable, using the string overload instead of the newAdd(string, bool?)overload that every other call site in this sweep switched to. Output is identical, so this is purely a leftover inconsistency, not a bug.@claude /review
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
@claude /review
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
Both follow-up commits since my last pass are small and directly resolve the only note I left (the leftover
.ToString().ToLowerInvariant()calls inServerClient.GetAccountInviteCodesAsyncnow use theXrpcParams.Add(string, bool?)overload like every other call site) plus a changelog accuracy fix (the line-count claim now matches the PR description's actual figures). No new code paths were touched, output is unchanged, and nothing new raises correctness or security concerns. Approving.⏱ 0m 26s · 6 turns · tokens: 78.0k in / 1.7k out (+548.6k cache-read)
📜 Full session transcript
✅ Verdict: approve — no blocking issues found.
Re-review of the same cleanup sweep. The one issue flagged in my previous pass —
ServerClient.GetAccountInviteCodesAsyncstill using the string-formatting overload (includeUsed?.ToString().ToLowerInvariant()) instead of the newXrpcParams.Add(string, bool?)overload — is now fixed; it uses.Add("includeUsed", includeUsed)/.Add("createAvailable", createAvailable)like every other call site.I re-checked the core claims in this diff:
XrpcParams/AddAllbuilder is applied consistently everywhere the description lists, including the previously-buggyAdminClient.GetAccountInfosAsync,LabelClient.QueryLabelsAsync(bothuriPatternsandsources), and the eight array filters on the twotools.ozonemoderation query methods — all now emit repeated keys instead of comma-joined strings.XrpcClient'sSendAsync/SendCoreAsync/SendCoreAsync<T>consolidation correctly threads aFunc<HttpContent>factory throughSendWithDPoPRetryAsync, which is necessary (not just cosmetic) since that path can replay the request on a DPoP nonce retry or 429, and a consumedHttpContentcan't be resent.EnsureSuccessAsyncnow parses rate-limit headers before the early-return, keepsResponseBodyon the non-envelope fallback exception, and narrows the swallow-all catch toJsonException or NotSupportedException— a real improvement over the previous bare catch.Replacehelper is correctly generalized to a sharedPdsParameterOverrides.Replace<TResource>used by both PDS resource types, preserving the original null-safety (current is ParameterResource) and remark comment.AtProtoClientFactory's new try/catch disposingoauthSession/clienton a failedApplyOAuthSessionAsync, andFileAtProtoTokenStore's temp-then-move write plus lock-guarded delete, both match what I verified in the prior pass and look unchanged here.Session.With(...)and theAtProtoHttpException.ResponseBodyinitaccessor are additive as claimed; no signature changes elsewhere in the visible diff.Nothing new in this diff looks like a correctness or security regression. Approving.
⏱ 1m 48s · 21 turns · tokens: 87.8k in / 7.0k out (+2165.3k cache-read)
📜 Full session transcript