{"record":{"id":"034a30f904bc1821","repo":"github/github-mcp-server","slug":"failed-to-download-logs-http-d","errorCode":null,"errorMessage":"failed to download logs: HTTP %d","messagePattern":"failed to download logs: HTTP (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/github/actions.go","lineNumber":179,"sourceCode":"\t\tresult[\"message\"] = \"Job logs are available for download\"\n\t\tresult[\"note\"] = \"The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content.\"\n\t}\n\n\treturn result, resp, nil\n}\n\nfunc downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLines int) (string, int, *http.Response, error) {\n\tprof := profiler.New(nil, profiler.IsProfilingEnabled())\n\tfinish := prof.Start(ctx, \"log_buffer_processing\")\n\n\thttpResp, err := http.Get(logURL) //nolint:gosec\n\tif err != nil {\n\t\treturn \"\", 0, httpResp, fmt.Errorf(\"failed to download logs: %w\", err)\n\t}\n\tdefer func() { _ = httpResp.Body.Close() }()\n\n\tif httpResp.StatusCode != http.StatusOK {\n\t\treturn \"\", 0, httpResp, fmt.Errorf(\"failed to download logs: HTTP %d\", httpResp.StatusCode)\n\t}\n\n\tbufferSize := min(tailLines, maxLines)\n\n\tprocessedInput, totalLines, httpResp, err := buffer.ProcessResponseAsRingBufferToEnd(httpResp, bufferSize)\n\tif err != nil {\n\t\treturn \"\", 0, httpResp, fmt.Errorf(\"failed to process log content: %w\", err)\n\t}\n\n\tlines := strings.Split(processedInput, \"\\n\")\n\tif len(lines) > tailLines {\n\t\tlines = lines[len(lines)-tailLines:]\n\t}\n\tfinalResult := strings.Join(lines, \"\\n\")\n\n\t_ = finish(len(lines), int64(len(finalResult)))\n\n\treturn finalResult, totalLines, httpResp, nil","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/pkg/github/actions.go#L161-L197","documentation":"Returned by downloadLogContent (pkg/github/actions.go:179) when the GET of the signed log URL completes but returns a status other than 200 - the body of that response is discarded and only the code is reported. Because GetWorkflowJobLogs redirects to a short-lived signed blob URL, the two dominant causes are an expired signature (403) and a deleted/purged blob (404).","triggerScenarios":"Delay between obtaining the signed URL and downloading it so the signature expires (403); log blob already garbage-collected after retention expiry (404); rare 5xx from blob storage; any interception layer answering with 4xx before GitHub storage is reached.","commonSituations":"Retried or cached log URLs reused minutes later; expired retention purging blobs while the API still lists the job; proxies returning 407/403 for storage domains.","solutions":["Do not cache or reuse the logs_url - fetch it fresh via GetWorkflowJobLogs on every attempt","On 404, accept the logs are purged (retention expired) and re-run the workflow if logs are needed","On 403 with an error page body, check proxy rules for objects.githubusercontent.com","Retry once with a fresh URL - expiry clears immediately"],"exampleFix":"// before\nurl, _, _ := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1)\ntime.Sleep(10 * time.Minute) // URL goes stale\nhttp.Get(url.String()) // -> HTTP 403\n\n// after - always fetch a fresh URL right before downloading\nurl, _, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1)\nif err != nil {\n    return err\n}\nresp, err := http.Get(url.String()) // use immediately\nif err == nil && resp.StatusCode != http.StatusOK {\n    url, _, _ = client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1) // refresh once\n    resp, err = http.Get(url.String())\n}","handlingStrategy":"retry","validationCode":"// Do not pre-validate a stale URL - validate your flow instead:\n// always fetch the URL and download it in the same turn\nurl, _, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1)\nif err != nil {\n    return err\n}\nresp, err := http.Get(url.String()) // immediately after fetching\n_ = resp","typeGuard":null,"tryCatchPattern":"if resp.StatusCode != http.StatusOK {\n    if resp.StatusCode == 403 {\n        url, _, _ = client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1) // fresh signature\n        resp, err = http.Get(url.String())\n    }\n    if resp.StatusCode == 404 {\n        return errors.New(\"log blob purged (retention expired)\") // not retryable\n    }\n}","preventionTips":["Treat logs URLs as single-use and short-lived","Export logs promptly after job completion for anything you may need later","Do not put signed URLs into queues or caches","On 403, refresh the URL rather than assuming a permission problem"],"tags":["http-status","signed-url","expiry","logs","go"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}