{"record":{"id":"4dfdaa761b32c176","repo":"github/github-mcp-server","slug":"failed-to-download-log-content-for-job-d-w","errorCode":null,"errorMessage":"failed to download log content for job %d: %w","messagePattern":"failed to download log content for job (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/github/actions.go","lineNumber":153,"sourceCode":"\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tresult := map[string]any{\n\t\t\"job_id\": jobID,\n\t}\n\tif jobName != \"\" {\n\t\tresult[\"job_name\"] = jobName\n\t}\n\n\tif returnContent {\n\t\t// Download and return the actual log content\n\t\tcontent, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp\n\t\tif err != nil {\n\t\t\tvar ghResp *github.Response\n\t\t\tif httpResp != nil {\n\t\t\t\tghResp = &github.Response{Response: httpResp}\n\t\t\t}\n\t\t\treturn nil, ghResp, fmt.Errorf(\"failed to download log content for job %d: %w\", jobID, err)\n\t\t}\n\t\tresult[\"logs_content\"] = content\n\t\tresult[\"message\"] = \"Job logs content retrieved successfully\"\n\t\tresult[\"original_length\"] = originalLength\n\t} else {\n\t\t// Return just the URL\n\t\tresult[\"logs_url\"] = url.String()\n\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","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/pkg/github/actions.go#L135-L171","documentation":"Returned by getJobLogData (pkg/github/actions.go:153) when downloadLogContent fails after the log URL was already obtained. It wraps one of three inner failures: transport error fetching the signed blob URL ('failed to download logs: %w'), a non-200 status ('failed to download logs: HTTP %d'), or a body-streaming error ('failed to process log content: %w'). The httpResp is converted to a github.Response so error-reporting middleware still sees status context.","triggerScenarios":"Calling action_job_logs with return_content=true where the follow-up GET of the signed URL (from the 302 redirect of GetWorkflowJobLogs) fails: network break, expired signed URL returning 403, blob deleted returning 404, or the ring-buffer reader hitting a mid-body error.","commonSituations":"Delay between getting the signed URL and fetching it (URLs are short-lived); egress blocking objects.githubusercontent.com; large logs cut off by proxies; token has API access but the environment cannot reach GitHub's blob storage.","solutions":["Unwrap the inner error to identify which of the three failures occurred, then apply its specific fix","Retry immediately - a fresh call re-obtains a fresh signed URL, fixing expiry-related 403s","Verify network egress to objects.githubusercontent.com","Reduce tail_lines to shrink the download window if transfers keep breaking"],"exampleFix":"// before\nif err != nil {\n    return err // opaque nested error\n}\n\n// after - branch on the wrapped cause\nvar ghErr *ghErrors.GitHubAPIError\nif errors.As(err, &ghErr) && ghErr.Response != nil {\n    switch ghErr.Response.StatusCode {\n    case 403:\n        return retryWithFreshURL() // signed URL expired\n    case 404:\n        return fmt.Errorf(\"log blob gone (expired retention)\")\n    }\n}","handlingStrategy":"retry","validationCode":"// Pre-check egress and freshness before requesting content\nif returnContent && !hostReachable(\"objects.githubusercontent.com\") {\n    return errors.New(\"blob storage unreachable; use return_content=false for URL only\")\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n    if strings.Contains(err.Error(), \"HTTP 403\") {\n        return retryWithFreshURL() // signed URL expired\n    }\n    if strings.Contains(err.Error(), \"HTTP 404\") {\n        return err // purged blob: not retryable\n    }\n    return retryOnce() // transport/streaming: transient\n}","preventionTips":["Never cache or reuse logs URLs - fetch a fresh one per download","Open egress to objects.githubusercontent.com where the server runs","Prefer return_content=false (URL only) when network to blob storage is restricted","Unwrap nested errors to find the inner download cause"],"tags":["network","download","signed-url","logs","go"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}