fix(platform): disable redirect following on meta and x/twitter clients#30
Conversation
PR SummaryMedium Risk Overview On Meta, the change goes further than no-follow alone: Twitter gets no-follow plus a shallow-copy override and tests; typed 3xx UNCONFIRMED there remains a follow-up (LFXV2-2642). Concept docs and the knowledge log are updated. Reviewed by Cursor Bugbot for commit e506639. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
Disables automatic redirects in Meta and X/Twitter Ads clients to reduce duplicate-create risk.
Changes:
- Adds enforced no-follow redirect policies.
- Preserves caller-supplied HTTP clients via shallow copies.
- Adds policy tests and a knowledge-log entry.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
internal/platform/meta/client.go |
Enforces no-follow redirects. |
internal/platform/meta/client_test.go |
Tests Meta redirect configuration. |
internal/platform/twitter/client.go |
Enforces no-follow redirects. |
internal/platform/twitter/client_test.go |
Tests X redirect configuration. |
docs/knowledge/log.md |
Records the redirect hardening. |
… (PR #30) Follow-up to the no-follow change: disabling redirect following surfaces a 3xx as an error, but Meta's createOutcomeAmbiguous only treated 5xx/transport as ambiguous — so a mutating POST that committed and then returned a 3xx was classified as a definite failure, and a caller could retry into a duplicate. Classify a mutating 3xx (300-399) as ambiguous alongside 5xx (a definite 4xx stays non-ambiguous). Added a table-driven createOutcomeAmbiguous test. (per copilot) Documented the enforced no-follow policy (and Meta's 3xx-UNCONFIRMED behavior) in the meta and twitter KB concept docs, per the repo doc convention for notable behavior changes. (per copilot) The X/Twitter equivalent is larger — that client has no typed error or createOutcomeAmbiguous helper at all (it returns a bare fmt.Errorf for non-2xx and echoes the body), so typing its error + adding ambiguity classification is a separate design change tracked as a follow-up (LFXV2-2642), noted in the twitter concept doc. Keeping this PR focused on the no-follow hardening + Meta's small 3xx classification. Verified: build, go test -race, golangci-lint clean. LFXV2-2641 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Review feedback addressed (round 2)Commit: e8042d9 Changes made
Deferred to follow-up
Verificationbuild / Threads resolved2 of 3 (twitter 3xx-classification deferred to LFXV2-2642). |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
internal/platform/meta/client.go:682
- Not every surfaced 3xx reaches this classifier.
doRequestreturns a plainfmt.Errorfwhen a non-2xx body is truncated (client.go:761-770) or exceeds the size cap (client.go:735-738), discarding the known status. A mutating POST that committed and then returned such a 3xx therefore makescreateOutcomeAmbiguousreturn false and can be blind-retried. Preserve the status/method in a typed error for these paths and cover the POST→truncated/oversized-3xx flow.
var ae *APIError
// A mutating 3xx (redirect, not followed) or 5xx may follow a committed create.
return errors.As(err, &ae) && ((ae.StatusCode >= 300 && ae.StatusCode < 400) || ae.StatusCode >= 500)
Address PR #30 review feedback from copilot[bot]: TestNoFollowRedirectPolicy (meta + twitter) injected a client with a nil CheckRedirect, so it would still pass if NewClient merely filled nil callbacks and preserved a caller-supplied redirect policy — leaving the unconditional override (the case that can re-enable redirect following) without regression coverage. - Inject a caller client carrying a SENTINEL CheckRedirect. - Assert the client the code actually uses returns http.ErrUseLastResponse despite the sentinel (proves the override is unconditional). - Assert the caller's original client still returns the sentinel (proves the shallow copy did not mutate it). Resolves 2 review threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
internal/platform/meta/client.go:682
- The new 3xx classification is bypassed when the redirect body is truncated or exceeds the response cap.
doRequestreturns a plainfmt.Errorffrom those body-failure paths before constructingAPIError, so this predicate returns false even though the mutating POST reached a responder and may have committed. Preserve the status/method for 3xx body failures (for example viaAPIErroror an ambiguous wrapper), and cover a POST→3xx response with a body read failure.
return errors.As(err, &ae) && ((ae.StatusCode >= 300 && ae.StatusCode < 400) || ae.StatusCode >= 500)
Address PR #30 review feedback from cursor: createOutcomeAmbiguous treated every 3xx APIError as UNCONFIRMED without checking the method, diverging from the reddit client (which gates 3xx on isMutatingMethod) despite the doc claiming to mirror it. All call sites pass POST today so behavior is unchanged, but the helper's contract was wrong for any future caller — a GET redirect is not a create and should not be UNCONFIRMED. - internal/platform/meta/client.go: added isMutatingMethod and gated the 3xx branch on it; 5xx and transport errors stay ambiguous regardless of method. - internal/platform/meta/client_test.go: extended the ambiguity test with GET/POST/DELETE method cases (GET 302 -> not ambiguous, GET 500 -> ambiguous). - docs/knowledge/log.md: log entries for this and the no-follow test hardening. Resolves 1 review thread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Align the X/Twitter createOutcomeAmbiguous with the meta (PR #30) and reddit clients after Cursor flagged the same divergence on meta: the predicate treated every 3xx as UNCONFIRMED without checking the method. A GET redirect is not a create and should not be ambiguous. - internal/platform/twitter/client.go: added isMutatingMethod and gated the 3xx branch on it; 5xx and transport errors stay ambiguous regardless of method. - internal/platform/twitter/client_test.go: added GET-302 (not ambiguous), DELETE-307 (ambiguous), and GET-500 (still ambiguous) cases. - docs/knowledge: corrected the classifier description (method-gated 3xx) and added a log entry. All three clients (reddit/meta/twitter) now share an identical method-gated create-outcome contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
…mbiguity Address PR #30 review feedback from copilot[bot]: - internal/platform/meta/client.go (doRequest): a NON-2xx response whose body fails to read was returned as a plain error, stripping the HTTP status. A mutating 3xx (surfaced because redirects aren't followed) or 5xx with an unreadable body means Meta may have committed the create, but createOutcomeAmbiguous keys on the *APIError status — so it was mis-classified as a definite failure. Now returns an *APIError preserving the status on a non-2xx read failure (2xx read failures still wrap as transportError). - internal/platform/meta/client.go (ad-set create): routed the error through createOutcomeAmbiguous like the campaign and ad/creative creates — an ambiguous 3xx/5xx now reads UNCONFIRMED (verify before retrying) instead of a definite "ad set creation failed", so a caller does not blind-retry into a duplicate. - internal/platform/meta/client_test.go: added TestDoRequestNon2xxReadErrorPreservesStatus and TestCreateCampaignAdSetAmbiguousIsUnconfirmed. - docs/knowledge/log.md: log entry. Resolves 2 review threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Address PR #30 review feedback from copilot[bot]: - internal/platform/meta/client.go: NewClient value-copied a WithHTTPClient- supplied *http.Client to override CheckRedirect. An http.Client must not be copied after first use — the copy duplicates its internal mutex while sharing the request-cancellation map, so concurrent use of the caller's client and the copy can race. Now builds a fresh *http.Client carrying only the exported reusable fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. The caller's client is neither copied nor mutated. - internal/platform/meta/client.go: the campaign UNCONFIRMED step said "timeout or server error"; a mutating 3xx now routes there too, so reworded to a reason- neutral "ambiguous response — timeout, server error, or an unfollowed redirect". - internal/platform/meta/client_test.go: the no-follow test now asserts the fresh client preserves the caller's Transport/Timeout and is a distinct pointer. Resolves 2 review threads. (The reddit client shares the value-copy pattern and will get the same fix as a follow-up; twitter gets it on PR #31.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 377c750. Configure here.
…ment Mirror the meta fix (PR #30): NewClient value-copied a WithHTTPClient-supplied *http.Client to override CheckRedirect, but an http.Client must not be copied after first use (the copy duplicates its internal mutex while sharing the request- cancellation map, so concurrent use of the caller's client and the copy can race). - internal/platform/twitter/client.go: build a fresh *http.Client carrying only the exported reusable fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. The caller's client is neither copied nor mutated. - internal/platform/twitter/client_test.go: the no-follow test now asserts the fresh client preserves the caller's Transport/Timeout and is a distinct pointer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
…ment Mirror the meta fix (PR #30): NewClient value-copied a WithHTTPClient-supplied *http.Client to override CheckRedirect, but an http.Client must not be copied after first use (the copy duplicates its internal mutex while sharing the request- cancellation map, so concurrent use of the caller's client and the copy can race). - internal/platform/twitter/client.go: build a fresh *http.Client carrying only the exported reusable fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. The caller's client is neither copied nor mutated. - internal/platform/twitter/client_test.go: the no-follow test now asserts the fresh client preserves the caller's Transport/Timeout and is a distinct pointer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
The Reddit client's NewClient value-copied a WithHTTPClient-supplied *http.Client (hc := *c.httpClient) to override CheckRedirect. An http.Client must not be copied after first use — go vet's copylocks analyzer flags it: the struct transitively holds sync primitives, so the copy duplicates its internal mutex while sharing the request-cancellation map, and concurrent use of the caller's client and the copy can race. reddit is the only client on main that enforces no-follow on a caller-supplied client (merged via PR #27), so it is the only one with this value-copy today. The same fresh-client construction is being made to meta (#30) and twitter (#31) as part of ADDING no-follow to those clients. - internal/platform/reddit/client.go: build a fresh *http.Client carrying only the exported reusable fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. The caller's client is neither copied nor mutated. Documented the copylocks rationale and the defensive nil guard. - internal/platform/reddit/client_test.go: assert the fresh client preserves the caller's Transport AND Jar (on top of Timeout / distinct-pointer / caller-not- mutated); reworded stale "copy" test comments to "fresh client". - docs/knowledge: corrected the reddit concept doc's "shallow copy" wording and the log entry (no phantom meta/twitter-on-main precedent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
…ment The Reddit client's NewClient applied its no-follow CheckRedirect to a WithHTTPClient-supplied client by value-copying it (hc := *c.httpClient). This now builds a fresh *http.Client carrying only the caller's documented exported fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. This is a defensive/clarity change, NOT a race fix: on the repo's Go target net/http.Client is exactly those four exported fields with no internal synchronization state, so the old value copy was also correct and go vet's copylocks analyzer does not flag it. The rebuild depends only on the type's public API rather than the struct's internal shape (layout-independent) and won't silently carry over any future unexported field. - internal/platform/reddit/client.go: fresh-client construction with an accurate rationale (no "must not be copied"/race claim); documented the defensive nil guard. - internal/platform/reddit/client_test.go: assert the client used preserves the caller's Transport/Jar/Timeout and the caller is not mutated. The test comment is explicit that it checks observable guarantees, not fresh-vs-copy (both yield a distinct pointer with the same fields). - docs/knowledge: reframed the concept doc + log entry as layout-independence and scoped the log entry to reddit (meta #30 / twitter #31 are separate open PRs). Scope: reddit only — it is the sole client on main enforcing no-follow on a caller-supplied client (merged via PR #27). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
…ment The Reddit client's NewClient applied its no-follow CheckRedirect to a WithHTTPClient-supplied client by value-copying it (hc := *c.httpClient). This now builds a fresh *http.Client carrying only the caller's documented exported fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. This is a defensive/clarity change, NOT a race fix: on the repo's Go target net/http.Client is exactly those four exported fields with no internal synchronization state, so the old value copy was also correct and go vet's copylocks analyzer does not flag it. The rebuild depends only on the type's public API rather than the struct's internal shape (layout-independent) and won't silently carry over any future unexported field. - internal/platform/reddit/client.go: fresh-client construction with an accurate rationale (no "must not be copied"/race claim); documented the defensive nil guard. - internal/platform/reddit/client_test.go: assert the client used preserves the caller's Transport/Jar/Timeout and the caller is not mutated. The test comment is explicit that it checks observable guarantees, not fresh-vs-copy (both yield a distinct pointer with the same fields). - docs/knowledge: reframed the concept doc + log entry as layout-independence and scoped the log entry to reddit (meta #30 / twitter #31 are separate open PRs). Scope: reddit only — it is the sole client on main enforcing no-follow on a caller-supplied client (merged via PR #27). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
…ts (LFXV2-2641) The Meta and X/Twitter Ads clients built their *http.Client (and accepted WithHTTPClient clients) without setting CheckRedirect, so the stdlib could follow a 3xx on a mutating POST after the create was already committed, muddying outcome classification — the same duplicate-create risk already fixed on the reddit, linkedin, and google-ads clients. For X/Twitter it's worse: a followed redirect resends an OAuth-1.0a request signed for the original URL to a different one. Add a shared noFollow CheckRedirect policy (returns http.ErrUseLastResponse) on the default client and enforce it UNCONDITIONALLY after options are applied — including on a WithHTTPClient-supplied client — via a shallow copy so the caller's client is not mutated. Mirrors the reddit client. Adds regression tests (default + injected client override without mutation). Verified: build, go test -race, golangci-lint clean. LFXV2-2641 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
… (PR #30) Follow-up to the no-follow change: disabling redirect following surfaces a 3xx as an error, but Meta's createOutcomeAmbiguous only treated 5xx/transport as ambiguous — so a mutating POST that committed and then returned a 3xx was classified as a definite failure, and a caller could retry into a duplicate. Classify a mutating 3xx (300-399) as ambiguous alongside 5xx (a definite 4xx stays non-ambiguous). Added a table-driven createOutcomeAmbiguous test. (per copilot) Documented the enforced no-follow policy (and Meta's 3xx-UNCONFIRMED behavior) in the meta and twitter KB concept docs, per the repo doc convention for notable behavior changes. (per copilot) The X/Twitter equivalent is larger — that client has no typed error or createOutcomeAmbiguous helper at all (it returns a bare fmt.Errorf for non-2xx and echoes the body), so typing its error + adding ambiguity classification is a separate design change tracked as a follow-up (LFXV2-2642), noted in the twitter concept doc. Keeping this PR focused on the no-follow hardening + Meta's small 3xx classification. Verified: build, go test -race, golangci-lint clean. LFXV2-2641 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Address PR #30 review feedback from copilot[bot]: TestNoFollowRedirectPolicy (meta + twitter) injected a client with a nil CheckRedirect, so it would still pass if NewClient merely filled nil callbacks and preserved a caller-supplied redirect policy — leaving the unconditional override (the case that can re-enable redirect following) without regression coverage. - Inject a caller client carrying a SENTINEL CheckRedirect. - Assert the client the code actually uses returns http.ErrUseLastResponse despite the sentinel (proves the override is unconditional). - Assert the caller's original client still returns the sentinel (proves the shallow copy did not mutate it). Resolves 2 review threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Address PR #30 review feedback from cursor: createOutcomeAmbiguous treated every 3xx APIError as UNCONFIRMED without checking the method, diverging from the reddit client (which gates 3xx on isMutatingMethod) despite the doc claiming to mirror it. All call sites pass POST today so behavior is unchanged, but the helper's contract was wrong for any future caller — a GET redirect is not a create and should not be UNCONFIRMED. - internal/platform/meta/client.go: added isMutatingMethod and gated the 3xx branch on it; 5xx and transport errors stay ambiguous regardless of method. - internal/platform/meta/client_test.go: extended the ambiguity test with GET/POST/DELETE method cases (GET 302 -> not ambiguous, GET 500 -> ambiguous). - docs/knowledge/log.md: log entries for this and the no-follow test hardening. Resolves 1 review thread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
…mbiguity Address PR #30 review feedback from copilot[bot]: - internal/platform/meta/client.go (doRequest): a NON-2xx response whose body fails to read was returned as a plain error, stripping the HTTP status. A mutating 3xx (surfaced because redirects aren't followed) or 5xx with an unreadable body means Meta may have committed the create, but createOutcomeAmbiguous keys on the *APIError status — so it was mis-classified as a definite failure. Now returns an *APIError preserving the status on a non-2xx read failure (2xx read failures still wrap as transportError). - internal/platform/meta/client.go (ad-set create): routed the error through createOutcomeAmbiguous like the campaign and ad/creative creates — an ambiguous 3xx/5xx now reads UNCONFIRMED (verify before retrying) instead of a definite "ad set creation failed", so a caller does not blind-retry into a duplicate. - internal/platform/meta/client_test.go: added TestDoRequestNon2xxReadErrorPreservesStatus and TestCreateCampaignAdSetAmbiguousIsUnconfirmed. - docs/knowledge/log.md: log entry. Resolves 2 review threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Address PR #30 review feedback from copilot[bot]: - internal/platform/meta/client.go: NewClient value-copied a WithHTTPClient- supplied *http.Client to override CheckRedirect. An http.Client must not be copied after first use — the copy duplicates its internal mutex while sharing the request-cancellation map, so concurrent use of the caller's client and the copy can race. Now builds a fresh *http.Client carrying only the exported reusable fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. The caller's client is neither copied nor mutated. - internal/platform/meta/client.go: the campaign UNCONFIRMED step said "timeout or server error"; a mutating 3xx now routes there too, so reworded to a reason- neutral "ambiguous response — timeout, server error, or an unfollowed redirect". - internal/platform/meta/client_test.go: the no-follow test now asserts the fresh client preserves the caller's Transport/Timeout and is a distinct pointer. Resolves 2 review threads. (The reddit client shares the value-copy pattern and will get the same fix as a follow-up; twitter gets it on PR #31.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
… doc Address PR #30 review feedback from copilot[bot]: - internal/platform/meta/client.go: the oversized-body branch (>1 MiB) returned a plain error before recording the status, so a mutating 3xx/5xx over the cap was still mis-classified as a definite failure (bypassing the non-2xx read-error fix from the previous commit). It now preserves the status the same way — 2xx wraps as transportError, non-2xx carries the status via *APIError — so an ambiguous outcome is never downgraded to a definite failure by an oversized body. - internal/platform/meta/client_test.go: added TestDoRequestOversizedNon2xxPreservesStatus. - docs/knowledge/code/internal-platform-meta.md: the concept doc still described a shallow copy; updated to document the fresh-client construction and the status preservation on unreadable/oversized bodies. Resolves 2 review threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
The Build and Test CI job's `make check-fmt` failed on comment alignment in client_test.go (test cases I added). Applied gofmt -w; no logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Address PR #30 review feedback from copilot[bot]: - internal/platform/meta/client.go: a 2xx ad-set response with an empty id fell through to a definite "returned no ad set ID" error, bypassing the ambiguity handling the error path uses. Meta may have created the ad set, so a blind retry could duplicate it — now surfaces UNCONFIRMED (verify before retrying), mirroring the campaign/ad and twitter no-id paths. - internal/platform/meta/client_test.go: added TestCreateCampaignAdSetNoIDIsUnconfirmed. - docs/knowledge/log.md: log entry. Resolves 1 review thread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
dee7629 to
24304a2
Compare
Align the X/Twitter createOutcomeAmbiguous with the meta (PR #30) and reddit clients after Cursor flagged the same divergence on meta: the predicate treated every 3xx as UNCONFIRMED without checking the method. A GET redirect is not a create and should not be ambiguous. - internal/platform/twitter/client.go: added isMutatingMethod and gated the 3xx branch on it; 5xx and transport errors stay ambiguous regardless of method. - internal/platform/twitter/client_test.go: added GET-302 (not ambiguous), DELETE-307 (ambiguous), and GET-500 (still ambiguous) cases. - docs/knowledge: corrected the classifier description (method-gated 3xx) and added a log entry. All three clients (reddit/meta/twitter) now share an identical method-gated create-outcome contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
…ment Mirror the meta fix (PR #30): NewClient value-copied a WithHTTPClient-supplied *http.Client to override CheckRedirect, but an http.Client must not be copied after first use (the copy duplicates its internal mutex while sharing the request- cancellation map, so concurrent use of the caller's client and the copy can race). - internal/platform/twitter/client.go: build a fresh *http.Client carrying only the exported reusable fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. The caller's client is neither copied nor mutated. - internal/platform/twitter/client_test.go: the no-follow test now asserts the fresh client preserves the caller's Transport/Timeout and is a distinct pointer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
Address review from copilot[bot]: - internal/platform/meta/client_test.go: TestDoRequestOversizedNon2xxPreservesStatus built a ~1 MiB payload, but maxResponseBody is 10 MiB, so the response took the ordinary 500 path and the test passed even if the oversized-body status- preservation branch were broken. Build the payload from maxResponseBody so it actually crosses the configured cap. - docs/knowledge/log.md: the entry said the oversized branch triggers at >1 MiB; maxResponseBody is 10 MiB. Corrected. Resolves 2 review threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>
| // | ||
| // Build a FRESH *http.Client rather than value-copying the caller's: an | ||
| // http.Client must not be copied after first use (a value copy duplicates its | ||
| // internal mutex while sharing the request-cancellation map, so concurrent use |
There was a problem hiding this comment.
[minor] Fresh-client rebuild is justified by a factually incorrect claim
Issue: The comment states an http.Client "must not be copied after first use (a value copy duplicates its internal mutex while sharing the request-cancellation map)". http.Client has no internal mutex and no request-cancellation map — its only fields are Transport, CheckRedirect, Jar, Timeout. The lock/connection state lives on http.Transport, which is shared by pointer in both the value-copy and fresh-client approaches.
Proof: go doc net/http.Client shows the four exported fields and nothing else; go vet ./internal/platform/twitter/ (which value-copies hc := *c.httpClient) passes clean — the copylocks analyzer would flag any struct embedding a sync.Locker.
Why it matters: The rebuild itself is harmless, but the rationale is the stated basis for diverging Meta from the reddit/twitter value-copy pattern this PR claims to "mirror," and the same incorrect claim is propagated into docs/knowledge/code/internal-platform-meta.md:96-97 and docs/knowledge/log.md:18-19. AGENTS.md asks the KB to stay accurate, so the myth shouldn't be enshrined there.
Fix: Either keep the fresh-client build but correct the reason (e.g. "build a fresh client so we set our own CheckRedirect without mutating the caller's client; carry over the reusable Transport/Jar/Timeout"), or revert to the value-copy that matches reddit/twitter. Update the two KB docs to match.
dealako
left a comment
There was a problem hiding this comment.
Review — @mrautela365
Hi @mrautela365 👋 — thorough, well-tested piece of hardening. This closes the last two clients (Meta + X/Twitter) that lacked a CheckRedirect guard, and the OAuth-1.0a angle for X is a genuinely good catch: with noFollow returning http.ErrUseLastResponse, Do returns the 3xx without issuing a second request, so a request signed for the original URL is never replayed against a redirect target. The Meta ambiguity work (method-gated 3xx, status preservation on unreadable/oversized bodies, ad-set 5xx + 2xx-no-id → UNCONFIRMED) is careful and each branch is backed by a real, reachable test. go build, go vet, and go test -race on both packages are clean, and the branch is mergeable.
I traced the correctness-critical paths independently and ran parallel security + code-quality passes. All prior Copilot/Cursor threads are addressed. The one substantive item is a documentation-accuracy problem, not a behavioral one.
Issue count
- 🔴 Blocking: 0
- 🟡 Minor: 1 — the fresh-client rebuild in
meta/client.gois justified by a factually incorrect claim (http.Client"duplicates its internal mutex while sharing the request-cancellation map"). It has no mutex or cancellation map — those are onhttp.Transport, shared by pointer either way. The same wrong claim is now in the KB concept doc andlog.md.go vetcopylocks passes on the value-copy, confirming it's safe. (inline) - ⚪ Nit: 1 — the fresh-client rebuild enumerates
Transport/CheckRedirect/Jar/Timeout; a value-copy (reddit/twitter) would carry any futurehttp.Clientfield automatically. Trivial robustness trade-off, take-it-or-leave-it. - ❔ Question: 1 — Meta was changed to build a fresh client while X/Twitter and reddit stay on value-copy (X deferred to #31). Since the value-copy is provably safe, deferring is fine — but the split makes a safe pattern look like a bug fix. Worth reconciling on one enforcement pattern across the three clients (already tracked, so just confirming it's intentional).
Data privacy / security
No exposure introduced. Auth travels in headers, never URLs; error types surface method/path/status, not the signed request URL or tokens; body reads stay bounded by maxResponseBody; test fixtures are obviously synthetic (tok-abc, ck/cs/at/ats, events.example.org).
Decision
✅ Approved with minor comments — the one minor and the parity question are documentation/consistency, not correctness. Nothing blocks merge; addressing the inaccurate http.Client rationale (in code + the two KB docs) before or right after merge would keep the knowledge base trustworthy.
…ment The Reddit client's NewClient applied its no-follow CheckRedirect to a WithHTTPClient-supplied client by value-copying it (hc := *c.httpClient). This now builds a fresh *http.Client carrying only the caller's documented exported fields (Transport, Jar, Timeout) with CheckRedirect: noFollow. This is a defensive/clarity change, NOT a race fix: on the repo's Go target net/http.Client is exactly those four exported fields with no internal synchronization state, so the old value copy was also correct and go vet's copylocks analyzer does not flag it. The rebuild depends only on the type's public API rather than the struct's internal shape (layout-independent) and won't silently carry over any future unexported field. - internal/platform/reddit/client.go: fresh-client construction with an accurate rationale (no "must not be copied"/race claim); documented the defensive nil guard. - internal/platform/reddit/client_test.go: assert the client used preserves the caller's Transport/Jar/Timeout and the caller is not mutated. The test comment is explicit that it checks observable guarantees, not fresh-vs-copy (both yield a distinct pointer with the same fields). - docs/knowledge: reframed the concept doc + log entry as layout-independence and scoped the log entry to reddit (meta #30 / twitter #31 are separate open PRs). Scope: reddit only — it is the sole client on main enforcing no-follow on a caller-supplied client (merged via PR #27). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org>

Problem
The Meta and X/Twitter Ads clients built their
*http.Client(and acceptedWithHTTPClientclients) with noCheckRedirect, so the Go stdlib follows redirects by default. On a mutating POST, the client could follow a 3xx after the create was already committed upstream and misclassify the outcome — the same duplicate-create risk already fixed on the reddit (#27), linkedin (#22), and google-ads (#29) clients. For X/Twitter it's worse: a followed redirect resends an OAuth 1.0a request signed for the original URL to a different target.This closes the last two clients that lacked the guard.
Changes
noFollowpolicy (http.ErrUseLastResponse) on the default client, and enforce it unconditionally after options are applied (including on aWithHTTPClient-supplied client) via a shallow copy so the caller's client is not mutated. Mirrors the reddit client'sNewClientenforcement.CheckRedirectis overridden without mutating the caller's client.Verification
go build,go test -race,golangci-lintall clean; existing tests unaffected (none relied on redirect-following). No new dependencies.LFXV2-2641