Skip to content

Commit fc15510

Browse files
committed
fix: race condition stale tokens/in flight requests
1 parent ef2373e commit fc15510

17 files changed

Lines changed: 542 additions & 394 deletions

pkg/connector/auth_recovery.go

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
type lineCallDeps[T any] struct {
1111
newClient func() *line.Client
12-
recover func(context.Context) error
12+
recover func(context.Context, *line.Client, error) (*line.Client, error)
1313
isAuthError func(error) bool
1414
call func(*line.Client) (T, error)
1515
}
@@ -27,13 +27,23 @@ func callLineWithRecovery[T any](ctx context.Context, client *line.Client, deps
2727
return client, res, err
2828
}
2929

30-
if errRecover := deps.recover(ctx); errRecover != nil {
30+
recoveredClient, errRecover := deps.recover(ctx, client, err)
31+
if errRecover != nil {
3132
var zero T
3233
return client, zero, fmt.Errorf("failed to recover token after LINE auth error (%w): %w", err, errRecover)
3334
}
35+
if recoveredClient == nil {
36+
return client, res, err
37+
}
3438

35-
client = deps.newClient()
39+
client = recoveredClient
3640
res, err = deps.call(client)
41+
if line.IsLoggedOut(err) {
42+
// The retry is the final attempt, but a current-token logout still needs
43+
// to transition the login to BAD_CREDENTIALS. The source-aware recovery
44+
// callback will ignore it if another token rotation made this retry stale.
45+
_, _ = deps.recover(ctx, client, err)
46+
}
3747
return client, res, err
3848
}
3949

@@ -44,10 +54,74 @@ func (lc *LineClient) isTokenError(err error) bool {
4454
if lc.isSessionInvalidated() {
4555
return false
4656
}
57+
return line.IsAuthError(err)
58+
}
59+
60+
// recoverClientAfterAuthError classifies an auth error using the exact client
61+
// that produced it. Logged-out responses from an older access token are safe to
62+
// retry after a concurrent refresh/re-login; the same response from the current
63+
// token is a genuine forced logout and must invalidate the session.
64+
func (lc *LineClient) recoverClientAfterAuthError(ctx context.Context, failedClient *line.Client, err error) (*line.Client, error) {
65+
if !lc.isTokenError(err) {
66+
return nil, nil
67+
}
68+
69+
// Wait behind any in-flight refresh/re-login before comparing tokens. This
70+
// makes the comparison authoritative even when the failed request completed
71+
// while another goroutine was rotating the access token.
72+
lc.recoverMu.Lock()
73+
if ctx.Err() != nil {
74+
lc.recoverMu.Unlock()
75+
return nil, ctx.Err()
76+
}
77+
78+
currentToken := lc.getAccessToken()
79+
if failedClient != nil && failedClient.AccessToken != "" && currentToken != "" && failedClient.AccessToken != currentToken && !lc.isSessionInvalidated() {
80+
if lc.UserLogin != nil && lc.UserLogin.Bridge != nil {
81+
lc.UserLogin.Bridge.Log.Debug().
82+
Bool("logged_out", line.IsLoggedOut(err)).
83+
Bool("stale_access_token", true).
84+
Msg("Retrying LINE request after response from stale access token")
85+
}
86+
lc.recoverMu.Unlock()
87+
return newLineAPIClient(currentToken), nil
88+
}
89+
4790
if line.IsLoggedOut(err) {
48-
return false
91+
lc.markLoggedOutByOtherClientLocked(ctx, err)
92+
lc.recoverMu.Unlock()
93+
return nil, nil
4994
}
50-
return line.IsAuthError(err)
95+
if lc.recoveryStopped || lc.superseded.Load() {
96+
lc.recoverMu.Unlock()
97+
return nil, errLineClientSuperseded
98+
}
99+
if lc.isSessionInvalidated() {
100+
lc.recoverMu.Unlock()
101+
return nil, errLineSessionInvalidated
102+
}
103+
lc.recoverMu.Unlock()
104+
105+
recoveryToken := lc.getAccessToken()
106+
if errRecover := recoverLineToken(lc, ctx); errRecover != nil {
107+
if line.IsLoggedOut(errRecover) {
108+
// Refresh/re-login errors come from the token that was current when
109+
// recovery started. Classify them with the same source-aware path in
110+
// case another serialized recovery rotated that token first.
111+
return lc.recoverClientAfterAuthError(ctx, newLineAPIClient(recoveryToken), errRecover)
112+
}
113+
return nil, errRecover
114+
}
115+
if ctx.Err() != nil {
116+
return nil, ctx.Err()
117+
}
118+
if lc.superseded.Load() {
119+
return nil, errLineClientSuperseded
120+
}
121+
if lc.isSessionInvalidated() {
122+
return nil, errLineSessionInvalidated
123+
}
124+
return lc.newClient(), nil
51125
}
52126

53127
func (lc *LineClient) callLine(ctx context.Context, call func(*line.Client) error) (*line.Client, error) {
@@ -57,15 +131,12 @@ func (lc *LineClient) callLine(ctx context.Context, call func(*line.Client) erro
57131
func (lc *LineClient) callLineUsing(ctx context.Context, client *line.Client, call func(*line.Client) error) (*line.Client, error) {
58132
client, _, err := callLineWithRecovery(ctx, client, lineCallDeps[struct{}]{
59133
newClient: func() *line.Client { return lc.newClient() },
60-
recover: func(ctx context.Context) error { return recoverLineToken(lc, ctx) },
134+
recover: lc.recoverClientAfterAuthError,
61135
isAuthError: lc.isTokenError,
62136
call: func(client *line.Client) (struct{}, error) {
63137
return struct{}{}, call(client)
64138
},
65139
})
66-
if lc.isLoggedOut(err) {
67-
lc.markLoggedOutByOtherClient(ctx, err)
68-
}
69140
return client, err
70141
}
71142

@@ -76,12 +147,9 @@ func callLineResult[T any](lc *LineClient, ctx context.Context, call func(*line.
76147
func callLineResultUsing[T any](lc *LineClient, ctx context.Context, client *line.Client, call func(*line.Client) (T, error)) (*line.Client, T, error) {
77148
client, res, err := callLineWithRecovery(ctx, client, lineCallDeps[T]{
78149
newClient: func() *line.Client { return lc.newClient() },
79-
recover: func(ctx context.Context) error { return recoverLineToken(lc, ctx) },
150+
recover: lc.recoverClientAfterAuthError,
80151
isAuthError: lc.isTokenError,
81152
call: call,
82153
})
83-
if lc.isLoggedOut(err) {
84-
lc.markLoggedOutByOtherClient(ctx, err)
85-
}
86154
return client, res, err
87155
}

0 commit comments

Comments
 (0)