{"record":{"id":"bae7ac28a82bd643","repo":"github/github-mcp-server","slug":"unexpected-status-d-s","errorCode":null,"errorMessage":"unexpected status %d: %s","messagePattern":"unexpected status (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/errors/error.go","lineNumber":219,"sourceCode":"\t\t_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr) // Explicitly ignore error for graceful handling\n\t}\n\treturn utils.NewToolResultErrorFromErr(message, err)\n}\n\n// NewGitHubRawAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware\nfunc NewGitHubRawAPIErrorResponse(ctx context.Context, message string, resp *http.Response, err error) *mcp.CallToolResult {\n\trawErr := newGitHubRawAPIError(message, resp, err)\n\tif ctx != nil {\n\t\t_, _ = addRawAPIErrorToContext(ctx, rawErr) // Explicitly ignore error for graceful handling\n\t}\n\treturn utils.NewToolResultErrorFromErr(message, err)\n}\n\n// NewGitHubAPIStatusErrorResponse handles cases where the API call succeeds (err == nil)\n// but returns an unexpected HTTP status code. It creates a synthetic error from the\n// status code and response body, then records it in context for observability tracking.\nfunc NewGitHubAPIStatusErrorResponse(ctx context.Context, message string, resp *github.Response, body []byte) *mcp.CallToolResult {\n\terr := fmt.Errorf(\"unexpected status %d: %s\", resp.StatusCode, string(body))\n\treturn NewGitHubAPIErrorResponse(ctx, message, resp, err)\n}\n\n// StructuredResolutionError is a machine-readable error returned by name-resolution\n// helpers (e.g. resolving a project field or single-select option by name). Agents\n// can parse the JSON body to self-correct without re-prompting.\n//\n// Kind values:\n//   - \"field_not_found\"      — no project field matches the supplied name\n//   - \"field_ambiguous\"      — more than one project field shares the supplied name\n//   - \"option_not_found\"     — no option on the resolved single-select field matches\n//   - \"option_ambiguous\"     — duplicate option names on the resolved field\n//   - \"item_not_in_project\"  — the issue/PR exists but is not an item on the project\n//   - \"wrong_field_type\"     — the named field is not the data type the caller expected\ntype StructuredResolutionError struct {\n\tKind       string `json:\"error\"`\n\tName       string `json:\"name,omitempty\"`\n\tField      string `json:\"field,omitempty\"`","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/pkg/errors/error.go#L201-L237","documentation":"Synthetic error built in NewGitHubAPIStatusErrorResponse (pkg/errors/error.go:219) when a go-github call returns err == nil but the HTTP status is not the expected success class. The status code and raw body are formatted as \"unexpected status %d: %s\" and the result is recorded as a GitHubAPIError for observability tracking. It means the transport succeeded while the API semantically refused or returned something surprising.","triggerScenarios":"API endpoints that succeed at the transport layer but return unexpected statuses: a reverse proxy injecting a 502 HTML page, a GHES appliance answering with a redirect or login page, an API change returning 3xx/204 where 200 was expected, or content-negotiation failures. Triggered by code paths that call NewGitHubAPIStatusErrorResponse after checking err == nil.","commonSituations":"Self-hosted deployments behind proxies that rewrite responses; GHES base URL configured without /api/v3 (or with it doubled); GitHub incidents serving error pages with 200-status oddities; API version drift between go-github and GitHub.","solutions":["Read the body embedded in the message text - it usually names the real cause (proxy error page, GHES login page)","Fix GHES base URL config: base REST URL must be like https://ghes.example.com/api/v3, upload URL similarly","Bypass or configure the proxy for api.github.com traffic","Update github-mcp-server and go-github if the API contract changed"],"exampleFix":"// before\nif err != nil {\n    return err // treats transport errors only\n}\n\n// after - also guard the success-status case\nif resp != nil && resp.StatusCode < 200 || resp.StatusCode >= 300 {\n    body, _ := io.ReadAll(resp.Body)\n    return fmt.Errorf(\"unexpected status %d: %s\", resp.StatusCode, string(body))\n}","handlingStrategy":"type-guard","validationCode":"// Before trusting a 'successful' call, assert the status class you expect\nif resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) {\n    body, _ := io.ReadAll(resp.Body)\n    return fmt.Errorf(\"unexpected status %d: %s\", resp.StatusCode, string(body))\n}","typeGuard":"func asUnexpectedStatus(err error) (code int, body string, ok bool) {\n    var ghErr *ghErrors.GitHubAPIError\n    if errors.As(err, &ghErr) && strings.HasPrefix(ghErr.Message, \"unexpected status\") {\n        // parse code from ghErr.Response or the synthetic message\n    }\n    return 0, \"\", false\n}","tryCatchPattern":"if err != nil {\n    if code, body, ok := asUnexpectedStatus(err); ok {\n        if code >= 500 || code == 502 {\n            backoffAndRetry() // proxy/upstream blip\n        } else {\n            log.Printf(\"api refused: %d body=%s\", code, body)\n        }\n    }\n    return err\n}","preventionTips":["Always validate both err and response status on API calls","Log response bodies for non-2xx statuses - they identify proxies and GHES pages","Configure GHES base URLs exactly (api/v3 suffix, no trailing path drift)","Keep reverse proxies from rewriting API responses"],"tags":["http-status","github-api","proxy","go"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}