Skip to content

Commit 642a13a

Browse files
committed
fix(github): StaticRoundTripper now owns token splitting and rotation for AccessToken connections
Previously, connection.Token (comma-separated PATs) was injected as-is into the Authorization header, sending "Bearer tok1,tok2,tok3" instead of a single rotated token. StaticRoundTripper now splits the raw token string on comma and rotates through tokens round-robin using an atomic counter. For REST: StaticRoundTripper operates at transport level and always overwrites the Authorization header set by SetupAuthentication. SetupAuthentication is retained because conn.tokens is still required by GetTokensCount() for rate limit calculation — but its header write is superseded by StaticRoundTripper on every request. For GraphQL: SetupAuthentication is never called by the graphql client, so StaticRoundTripper is the only auth mechanism on this path — without this fix, GraphQL requests were sent with the full unsplit token string.
1 parent abf28cf commit 642a13a

1 file changed

Lines changed: 19 additions & 7 deletions

File tree

backend/plugins/github/token/round_tripper.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package token
1919

2020
import (
2121
"net/http"
22+
"strings"
23+
"sync/atomic"
2224
)
2325

2426
// RefreshRoundTripper is an HTTP transport middleware that automatically manages OAuth token refreshes.
@@ -97,22 +99,32 @@ func (rt *RefreshRoundTripper) roundTripWithRetry(req *http.Request, refreshAtte
9799
// StaticRoundTripper is an HTTP transport that injects a fixed bearer token.
98100
// Unlike RefreshRoundTripper, it does NOT attempt refresh or retries.
99101
type StaticRoundTripper struct {
100-
base http.RoundTripper
101-
token string
102+
base http.RoundTripper
103+
tokens []string
104+
idx atomic.Uint64
102105
}
103106

104-
func NewStaticRoundTripper(base http.RoundTripper, token string) *StaticRoundTripper {
107+
func NewStaticRoundTripper(base http.RoundTripper, rawToken string) *StaticRoundTripper {
105108
if base == nil {
106109
base = http.DefaultTransport
107110
}
108-
return &StaticRoundTripper{
109-
base: base,
110-
token: token,
111+
parts := strings.Split(rawToken, ",")
112+
tokens := make([]string, 0, len(parts))
113+
for _, t := range parts {
114+
if t = strings.TrimSpace(t); t != "" {
115+
tokens = append(tokens, t)
116+
}
117+
}
118+
if len(tokens) == 0 {
119+
tokens = []string{rawToken}
111120
}
121+
return &StaticRoundTripper{base: base, tokens: tokens}
112122
}
113123

114124
func (rt *StaticRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
125+
// always overrides headers put by SetupAuthentication, to make sure the token is always injected
126+
tok := rt.tokens[rt.idx.Add(1)%uint64(len(rt.tokens))]
115127
reqClone := req.Clone(req.Context())
116-
reqClone.Header.Set("Authorization", "Bearer "+rt.token)
128+
reqClone.Header.Set("Authorization", "Bearer "+tok)
117129
return rt.base.RoundTrip(reqClone)
118130
}

0 commit comments

Comments
 (0)