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

  1. 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).
  2. Verify the TenantURL is correct and reachable: curl the /oauth/device/code endpoint directly to inspect the raw response body and status.
  3. 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.
  4. 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

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


AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07). Data as JSON: /api/errors/aab1d55801c7f245. Report an issue: GitHub.