docker/cli · error
unexpected response from tenant: {status}
Error message
unexpected response from tenant: {status} What it means
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.
Source
Thrown at internal/oauth/api/api.go:91
}
var state State
err = json.NewDecoder(resp.Body).Decode(&state)
if err != nil {
return state, fmt.Errorf("failed to get device code: %w", err)
}
return state, nil
}
func tryDecodeOAuthError(resp *http.Response) error {
var body map[string]any
if err := json.NewDecoder(resp.Body).Decode(&body); err == nil {
if errorDescription, ok := body["error_description"].(string); ok {
return errors.New(errorDescription)
}
}
return errors.New("unexpected response from tenant: " + resp.Status)
}
// WaitForDeviceToken polls the tenant to get access/refresh tokens for the user.
// This should be called after GetDeviceCode, and will block until the user has
// authenticated or we have reached the time limit for authenticating (based on
// the response from GetDeviceCode).
func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
// Ticker for polling tenant for login – based on the interval
// specified by the tenant response.
ticker := time.NewTimer(state.IntervalDuration())
defer ticker.Stop()
// The tenant tells us for as long as we can poll it for credentials
// while the user logs in through their browser. Timeout if we don't get
// credentials within this period.
timeout := time.NewTimer(state.ExpiryDuration())
defer timeout.Stop()
for {View on GitHub (pinned to 4f84911bfe)
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.
Example fix
// before: no retry, opaque failure
state, err := api.GetDeviceCode(ctx, audience)
if err != nil {
return err // 'unexpected response from tenant: 503 Service Unavailable'
}
// after: inspect and retry transient errors
state, err := api.GetDeviceCode(ctx, audience)
if err != nil {
if strings.Contains(err.Error(), "unexpected response from tenant") {
// tenant returned non-standard response; retry with backoff
time.Sleep(2 * time.Second)
state, err = api.GetDeviceCode(ctx, audience)
}
if err != nil {
return fmt.Errorf("device code flow unavailable: %w", err)
}
} Defensive patterns
Strategy: retry
Validate before calling
// Verify tenant reachability before starting the flow
func checkTenantReachable(ctx context.Context, tenantURL string) error {
req, err := http.NewRequestWithContext(ctx, "HEAD", tenantURL, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("tenant %s unreachable: %w", tenantURL, err)
}
resp.Body.Close()
if resp.StatusCode >= 500 {
return fmt.Errorf("tenant returning server error: %s", resp.Status)
}
return nil
} Try / catch
// Retry transient tenant errors with backoff
state, err := getDeviceCodeWithRetry(ctx, api, audience, 3)
func getDeviceCodeWithRetry(ctx context.Context, api OAuthAPI, audience string, maxRetries int) (State, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
state, err := api.GetDeviceCode(ctx, audience)
if err == nil {
return state, nil
}
lastErr = err
if strings.Contains(err.Error(), "unexpected response from tenant") {
select {
case <-time.After(time.Duration(i+1) * 2 * time.Second):
case <-ctx.Done():
return State{}, ctx.Err()
}
continue
}
return State{}, err // non-retryable
}
return State{}, lastErr
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- timed out waiting for device token
- failed to start device code flow login
- login canceled
- unable to prepare context: %w
- error establishing connection to trust repository: %w
AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07).
Data as JSON: /api/errors/aab1d55801c7f245.
Report an issue: GitHub.