From 52f9ec967cdfc43a50cc70d60dd00b6b8efda5a1 Mon Sep 17 00:00:00 2001 From: chiyi Date: Mon, 10 Aug 2026 09:15:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(leonardo):=20get-session=20=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=20null=20=E6=97=B6=E9=87=8D=E8=AF=95=203=20=E6=AC=A1?= =?UTF-8?q?=E5=86=8D=E5=88=A4=20cookie=20=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 预热成功后的 200 null 之前第一次就 break 并直接 ErrAuth;同样的响应也可能是某个出口 IP 被挑战页静默降级,改成最多 3 次(第 2、3 次走代理换 IP)都拿不到 accessToken 才算死号。 --- backend/internal/provider/leonardo/client.go | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/backend/internal/provider/leonardo/client.go b/backend/internal/provider/leonardo/client.go index 42a53a6..ef41bfe 100644 --- a/backend/internal/provider/leonardo/client.go +++ b/backend/internal/provider/leonardo/client.go @@ -40,6 +40,12 @@ const ( // attempt is retried, falling back to the proxy for a different exit IP. getSessionAttempts = 10 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 ( @@ -229,6 +235,9 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error } status, body, setCookies, err = c.fetchSession(ctx, client, send) 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 } } @@ -349,6 +358,20 @@ func (c *Client) fetchSession(ctx context.Context, client tlsclient.HttpClient, 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 // 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