docker/cli · error
failed to decode response
Error message
failed to decode response: %w
What it means
Returned by getDeviceToken() when json.NewDecoder(resp.Body).Decode(&res) fails. The token endpoint returned an HTTP response (no transport error, status not checked here because 403 authorization_pending is normal) but the body is not valid JSON or does not match TokenResponse.
Solutions
- Confirm the TenantURL is correct and reachable directly (curl the /oauth/token endpoint).
- Bypass captive portals / proxies that rewrite responses to HTML.
- Retry the login flow once transient intermediary issues clear.
- Update the CLI to match the current Hub OAuth contract.
Example fix
# before: captive portal returns HTML docker login # after: connect to real network / configure proxy auth wifi-connect trusted-network docker login
Defensive patterns
Strategy: try-catch
Try / catch
var se *json.SyntaxError
var te *json.UnmarshalTypeError
switch {
case errors.As(err, &se): // body not JSON (captive portal)
case errors.As(err, &te): // schema mismatch
} Prevention
- Ensure no proxy rewrites the OAuth response body to HTML.
- Keep the CLI version aligned with the Hub OAuth schema.
- Retry once after moving off captive networks.
When it happens
Trigger: The OAuth tenant returns HTML/garbage (captive portal, proxy error page, 5xx with text body) instead of the expected JSON token/error document during the device-code grant polling.
Common situations: Captive Wi-Fi portal intercepting the POST; reverse proxy returning an HTML error; tenant endpoint changed schema; truncated response from a flaky proxy.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to get tokens
- unexpected response from Hub
- failed waiting for authentication
- failed to parse token claims
- failed to store tokens
AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07).
Data as JSON: /api/errors/237668645fb0262a.
Report an issue: GitHub.
Appendix: source
Thrown at internal/oauth/api/api.go:184
"device_code": {state.DeviceCode},
}
oauthTokenURL := a.TenantURL + "/oauth/token"
resp, err := postForm(ctx, oauthTokenURL, strings.NewReader(data.Encode()))
if err != nil {
return TokenResponse{}, fmt.Errorf("failed to get tokens: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
// this endpoint returns a 403 with an `authorization_pending` error until the
// user has authenticated, so we don't check the status code here and instead
// decode the response and check for the error.
var res TokenResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return res, fmt.Errorf("failed to decode response: %w", err)
}
return res, nil
}
// RevokeToken revokes a refresh token with the tenant so that it can no longer
// be used to get new tokens.
func (a API) RevokeToken(ctx context.Context, refreshToken string) error {
data := url.Values{
"client_id": {a.ClientID},
"token": {refreshToken},
}
revokeURL := a.TenantURL + "/oauth/revoke"
resp, err := postForm(ctx, revokeURL, strings.NewReader(data.Encode()))
if err != nil {
return err
}View on GitHub (pinned to 4f84911bfe)