99
1010type 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
53127func (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
57131func (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.
76147func 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