fix(leonardo): get-session 返回 null 时重试 3 次再判 cookie 失效

预热成功后的 200 null 之前第一次就 break 并直接 ErrAuth;同样的响应也可能是某个出口 IP 被挑战页静默降级,改成最多 3 次(第 2、3 次走代理换 IP)都拿不到 accessToken 才算死号。
This commit is contained in:
2026-08-10 09:15:07 +08:00
parent 265ff1dd5d
commit 52f9ec967c
@@ -40,6 +40,12 @@ const (
// attempt is retried, falling back to the proxy for a different exit IP. // attempt is retried, falling back to the proxy for a different exit IP.
getSessionAttempts = 10 getSessionAttempts = 10
getSessionRetryDelay = 2 * time.Second getSessionRetryDelay = 2 * time.Second
// A warmed 200 null usually means the session is gone server-side, but the
// same answer also comes back when the checkpoint silently serves a session-
// less page to an exit IP — so retry it a few times (later attempts through
// the proxy) before calling the account dead. Fewer attempts than the 429
// budget: each one costs two requests and a truly dead cookie never recovers.
getSessionNullAttempts = 3
) )
var ( var (
@@ -229,6 +235,9 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error
} }
status, body, setCookies, err = c.fetchSession(ctx, client, send) status, body, setCookies, err = c.fetchSession(ctx, client, send)
if err == nil && warmed && status != 429 && status != 403 { if err == nil && warmed && status != 429 && status != 403 {
if status == 200 && sessionAccessToken(body) == "" && attempt < getSessionNullAttempts-1 {
continue // retry a null session on another exit IP before giving up
}
break break
} }
} }
@@ -349,6 +358,20 @@ func (c *Client) fetchSession(ctx context.Context, client tlsclient.HttpClient,
return resp.StatusCode, body, resp.Header["Set-Cookie"], nil return resp.StatusCode, body, resp.Header["Set-Cookie"], nil
} }
// sessionAccessToken pulls the bearer out of a get-session body; "" means the
// answer carried none (a null session, or a session without a token).
func sessionAccessToken(body []byte) string {
var raw struct {
Session struct {
AccessToken string `json:"accessToken"`
} `json:"session"`
}
if err := json.Unmarshal(body, &raw); err != nil {
return ""
}
return strings.TrimSpace(raw.Session.AccessToken)
}
// sessionHeader is the auth endpoints' request shape, copied from a real // sessionHeader is the auth endpoints' request shape, copied from a real
// browser's call (HAR): a same-origin GET carries NO origin header and DOES // browser's call (HAR): a same-origin GET carries NO origin header and DOES
// carry the ua client hints + priority — sending origin while omitting the // carry the ua client hints + priority — sending origin while omitting the