amir20/dozzle · error
failed to decode token response
Error message
failed to decode token response
What it means
After a 200 response, cloudCallback decodes the JSON body into exchangeTokenResponse. If the body is not valid JSON or has an incompatible shape, json.Decode fails and dozzle returns 500 with this message. The precise decode error is logged server-side.
Solutions
- Curl the exchange endpoint with the same token to inspect the raw 200 response body.
- Confirm DOLIGENCE_URL points at the actual API service, not an HTML page or proxy intercept.
- Bypass any proxy that rewrites HTTPS responses and retry.
- If the cloud API schema changed, upgrade dozzle to a matching version.
Example fix
// before (raw 200 body)
<html>login required</html>
// after
{"key":"<key>","expiresAt":"2026-01-01T00:00:00Z"} Defensive patterns
Strategy: type-guard
Validate before calling
const body = await res.text();
try { const j = JSON.parse(body); } catch { throw new Error('exchange response is not JSON: ' + body.slice(0, 200)); } Type guard
function isTokenResponse(v: unknown): v is { key: string; expiresAt?: string } {
return typeof v === 'object' && v !== null && typeof (v as any).key === 'string';
} Try / catch
try {
const data = JSON.parse(body);
} catch (e) {
// response was HTML/empty; inspect for proxy interference
} Prevention
- Ensure no proxy intercepts and rewrites responses to the exchange endpoint.
- Point DOLIGENCE_URL at the JSON API, not an HTML page.
- Keep dozzle and the cloud service versions compatible.
When it happens
Trigger: The exchange endpoint returns 200 with a non-JSON body: an HTML error/login page from an intercepting proxy, an empty body, a text error, or JSON whose `key` field has a non-string type.
Common situations: Corporate proxy or captive portal rewriting responses; custom DOLIGENCE_URL pointing at an app that returns HTML; truncated response; cloud service returning a changed response schema after an update.
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 parse arguments
- cloud search failed
- cloud dispatcher missing
- cloud: no API key configured
- cloud: alerts
AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07).
Data as JSON: /api/errors/e09b4882122b36ed.
Report an issue: GitHub.
Appendix: source
Thrown at internal/web/cloud.go:89
resp, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Failed to exchange token")
http.Error(w, "failed to exchange token", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
log.Error().Int("status", resp.StatusCode).Str("body", string(body)).Msg("Token exchange failed")
http.Error(w, "token exchange failed", http.StatusInternalServerError)
return
}
var tokenResp exchangeTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
log.Error().Err(err).Msg("Failed to decode token response")
http.Error(w, "failed to decode token response", http.StatusInternalServerError)
return
}
if tokenResp.Key == "" {
log.Error().Msg("Empty key received")
http.Error(w, "empty key received", http.StatusInternalServerError)
return
}
var expiresAt *time.Time
if tokenResp.ExpiresAt != nil {
parsed, err := time.Parse(time.RFC3339, *tokenResp.ExpiresAt)
if err != nil {
log.Warn().Err(err).Str("expiresAt", *tokenResp.ExpiresAt).Msg("Failed to parse expiresAt, ignoring")
} else {
expiresAt = &parsed
}
}View on GitHub (pinned to d9463cbe21)