{"record":{"id":"aab1d55801c7f245","repo":"docker/cli","slug":"unexpected-response-from-tenant-status","errorCode":null,"errorMessage":"unexpected response from tenant: {status}","messagePattern":"unexpected response from tenant: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/oauth/api/api.go","lineNumber":91,"sourceCode":"\t}\n\n\tvar state State\n\terr = json.NewDecoder(resp.Body).Decode(&state)\n\tif err != nil {\n\t\treturn state, fmt.Errorf(\"failed to get device code: %w\", err)\n\t}\n\n\treturn state, nil\n}\n\nfunc tryDecodeOAuthError(resp *http.Response) error {\n\tvar body map[string]any\n\tif err := json.NewDecoder(resp.Body).Decode(&body); err == nil {\n\t\tif errorDescription, ok := body[\"error_description\"].(string); ok {\n\t\t\treturn errors.New(errorDescription)\n\t\t}\n\t}\n\treturn errors.New(\"unexpected response from tenant: \" + resp.Status)\n}\n\n// WaitForDeviceToken polls the tenant to get access/refresh tokens for the user.\n// This should be called after GetDeviceCode, and will block until the user has\n// authenticated or we have reached the time limit for authenticating (based on\n// the response from GetDeviceCode).\nfunc (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) {\n\t// Ticker for polling tenant for login – based on the interval\n\t// specified by the tenant response.\n\tticker := time.NewTimer(state.IntervalDuration())\n\tdefer ticker.Stop()\n\t// The tenant tells us for as long as we can poll it for credentials\n\t// while the user logs in through their browser. Timeout if we don't get\n\t// credentials within this period.\n\ttimeout := time.NewTimer(state.ExpiryDuration())\n\tdefer timeout.Stop()\n\n\tfor {","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/docker/cli/blob/4f84911bfe8811e9b028e4b1fee8e7510be79387/internal/oauth/api/api.go#L73-L109","documentation":"Fallback error produced by tryDecodeOAuthError in internal/oauth/api/api.go:91 when the Auth0 tenant returns a non-200 HTTP status whose response body is either not valid JSON or does not contain an 'error_description' string field. The raw HTTP status text (e.g. '500 Internal Server Error') is appended so the caller has some idea what went wrong. It is the catch-all for responses that don't conform to the standard OAuth error schema.","triggerScenarios":"A POST to /oauth/device/code (GetDeviceCode) or /oauth/revoke (RevokeToken) returns a non-200 status, and json.Decode of the body either fails or the decoded map lacks a string 'error_description' key. This happens when the tenant serves an HTML error/maintenance page, a proxy intercepts the request, or the tenant is rate-limiting with a non-JSON body.","commonSituations":"Tenant outage or maintenance window serving HTML, corporate proxy/firewall returning a block page, wrong TenantURL configuration pointing at a non-Auth0 host, CDN/gateway 502/503 responses, or rate-limiting responses that omit the standard OAuth error JSON.","solutions":["Check the appended HTTP status code — a 5xx indicates a tenant-side issue worth retrying after a brief wait; a 4xx suggests a configuration problem (wrong ClientID, wrong audience).","Verify the TenantURL is correct and reachable: curl the /oauth/device/code endpoint directly to inspect the raw response body and status.","If behind a corporate proxy, ensure HTTPS_PROXY/HTTP_PROXY env vars are set so the request reaches Auth0 rather than a proxy block page.","Retry with exponential backoff for transient 5xx/502/503/429 responses; log the full resp.Status for debugging."],"exampleFix":"// before: no retry, opaque failure\nstate, err := api.GetDeviceCode(ctx, audience)\nif err != nil {\n    return err // 'unexpected response from tenant: 503 Service Unavailable'\n}\n\n// after: inspect and retry transient errors\nstate, err := api.GetDeviceCode(ctx, audience)\nif err != nil {\n    if strings.Contains(err.Error(), \"unexpected response from tenant\") {\n        // tenant returned non-standard response; retry with backoff\n        time.Sleep(2 * time.Second)\n        state, err = api.GetDeviceCode(ctx, audience)\n    }\n    if err != nil {\n        return fmt.Errorf(\"device code flow unavailable: %w\", err)\n    }\n}","handlingStrategy":"retry","validationCode":"// Verify tenant reachability before starting the flow\nfunc checkTenantReachable(ctx context.Context, tenantURL string) error {\n    req, err := http.NewRequestWithContext(ctx, \"HEAD\", tenantURL, nil)\n    if err != nil {\n        return err\n    }\n    resp, err := http.DefaultClient.Do(req)\n    if err != nil {\n        return fmt.Errorf(\"tenant %s unreachable: %w\", tenantURL, err)\n    }\n    resp.Body.Close()\n    if resp.StatusCode >= 500 {\n        return fmt.Errorf(\"tenant returning server error: %s\", resp.Status)\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"// Retry transient tenant errors with backoff\nstate, err := getDeviceCodeWithRetry(ctx, api, audience, 3)\n\nfunc getDeviceCodeWithRetry(ctx context.Context, api OAuthAPI, audience string, maxRetries int) (State, error) {\n    var lastErr error\n    for i := 0; i < maxRetries; i++ {\n        state, err := api.GetDeviceCode(ctx, audience)\n        if err == nil {\n            return state, nil\n        }\n        lastErr = err\n        if strings.Contains(err.Error(), \"unexpected response from tenant\") {\n            select {\n            case <-time.After(time.Duration(i+1) * 2 * time.Second):\n            case <-ctx.Done():\n                return State{}, ctx.Err()\n            }\n            continue\n        }\n        return State{}, err // non-retryable\n    }\n    return State{}, lastErr\n}","preventionTips":["Set DOCKER_CLI_DEBUG=1 or enable logrus debug to capture the full HTTP status in the 'unexpected response' message.","Verify TenantURL configuration at startup with a HEAD/GET request before entering the device-code flow.","Configure HTTPS_PROXY/HTTP_PROXY if behind a corporate network to avoid proxy block pages being mistaken for tenant responses.","Implement retry with exponential backoff for 5xx/502/503/429 status codes."],"tags":["oauth","authentication","network","http","auth0"],"backgroundTag":null,"analyzedSha":"4f84911bfe8811e9b028e4b1fee8e7510be79387","analyzedAt":"2026-08-07T12:15:29.814Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}