{"record":{"id":"5b672174fe52e2ab","repo":"fish2018/pansou","slug":"http-d-5b6721","errorCode":null,"errorMessage":"HTTP 状态码 %d","messagePattern":"HTTP 状态码 (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/jsnoteclub/jsnoteclub.go","lineNumber":509,"sourceCode":"\t\t}\n\t}\n\treturn matched\n}\n\nfunc (p *JsNoteClubPlugin) doRequestWithRetry(req *http.Request, client *http.Client, maxRetries int) (*http.Response, error) {\n\tvar lastErr error\n\n\tfor attempt := 0; attempt < maxRetries; attempt++ {\n\t\tresp, err := client.Do(req.Clone(req.Context()))\n\t\tif err == nil && resp.StatusCode == http.StatusOK {\n\t\t\treturn resp, nil\n\t\t}\n\t\tif resp != nil {\n\t\t\tresp.Body.Close()\n\t\t}\n\t\tlastErr = err\n\t\tif err == nil {\n\t\t\tlastErr = fmt.Errorf(\"HTTP 状态码 %d\", resp.StatusCode)\n\t\t}\n\t\tif attempt < maxRetries-1 {\n\t\t\ttime.Sleep(retryBaseDelay * time.Duration(1<<attempt))\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"重试 %d 次后失败: %w\", maxRetries, lastErr)\n}\n\nfunc newHTTPClient() *http.Client {\n\treturn &http.Client{\n\t\tTimeout: requestTimeout,\n\t\tTransport: &http.Transport{\n\t\t\tMaxIdleConns:        httpMaxIdleConns,\n\t\t\tMaxIdleConnsPerHost: httpMaxIdlePerHost,\n\t\t\tMaxConnsPerHost:     httpMaxConnsPerHost,\n\t\t\tIdleConnTimeout:     90 * time.Second,\n\t\t\tTLSHandshakeTimeout: 10 * time.Second,","sourceCodeStart":491,"sourceCodeEnd":527,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/jsnoteclub/jsnoteclub.go#L491-L527","documentation":"This error is created inside doRequestWithRetry when client.Do succeeded (err == nil) but the response status was not 200 after every retry. The last non-200 status code is recorded as lastErr and ultimately returned wrapped as '重试 3 次后失败: HTTP 状态码 %d'. It represents the upstream server answering, but with an unacceptable status (403, 429, 5xx, etc.).","triggerScenarios":"All retry attempts receive a response with StatusCode != http.StatusOK from the target (homepage, posts API, or a detail page). Callers fetchDataKey, fetchPosts, and fetchDetailLinks all route through this; e.g. a detail page returns 404 or the API returns 403 due to bot blocking.","commonSituations":"Cloudflare/bot protection blocking datacenter IPs with 403; detail post URLs returning 404 after posts were deleted; rate limiting (429) from aggressive parallel detail fetches (8 workers × retries); temporary 502/503 during site maintenance.","solutions":["Read the status code from the message and react: 403 → rotate User-Agent/Referer or reduce request rate; 429 → increase retryBaseDelay and reduce maxDetailWorkers; 404 → drop the dead URL; 5xx → retry later.","Note a bug in the source: when err != nil, resp is nil and the code accesses resp.StatusCode only when err == nil, but on transport errors lastErr may be a wrapped 'client.Do' error — ensure you surface the wrapped error via errors.Unwrap to find the true cause.","Slow down concurrency (maxDetailWorkers=8 parallel detail fetches) if you see 429s.","Use the same headers a browser would send (setHTMLHeaders/setAPIHeaders already do this) and verify with curl whether the block is IP-based; consider a proxy if your host IP is blocked.","Increase maxRequestRetries or use jittered exponential backoff for transient 5xx."],"exampleFix":"// before\nlastErr = err\nif err == nil {\n    lastErr = fmt.Errorf(\"HTTP 状态码 %d\", resp.StatusCode)\n}\n// after\nlastErr = err\nif err == nil {\n    lastErr = fmt.Errorf(\"HTTP 状态码 %d\", resp.StatusCode)\n}\nif resp != nil && (resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500) {\n    // retryable — increase backoff for these statuses\n    time.Sleep(retryBaseDelay * time.Duration(1<<attempt))\n}","handlingStrategy":"retry","validationCode":"// pre-check the URL responds 200 before heavy parallel fetching\nresp, err := client.Head(detailURL)\nif err == nil && resp.StatusCode == http.StatusNotFound {\n    log.Printf(\"skipping dead detail page: %s\", detailURL)\n    return\n}","typeGuard":null,"tryCatchPattern":"resp, err := p.doRequestWithRetry(req, client, maxRequestRetries)\nif err != nil {\n    if strings.Contains(err.Error(), \"状态码 429\") || strings.Contains(err.Error(), \"状态码 403\") {\n        // blocked/rate-limited — wait longer before the next attempt\n        time.Sleep(30 * time.Second)\n    }\n    return nil, err\n}","preventionTips":["Throttle concurrency (maxDetailWorkers) and add jittered backoff to avoid 429 rate limiting.","Send full browser-like headers; if a datacenter IP is blocked (403), route through a proxy.","Skip URLs that repeatedly return 404 instead of retrying them.","Distinguish retryable (429, 5xx) from non-retryable (401, 403, 404) statuses before spending retry budget."],"tags":["http","status-code","retry","rate-limit"],"backgroundTag":"http-error-response","analyzedSha":"beaa56133755a548ebc51b090b3816e2ae044aa6","analyzedAt":"2026-09-07T00:31:18.025Z","contentChangedAt":"2026-09-07T00:31:18.025Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}