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

  1. Curl the exchange endpoint with the same token to inspect the raw 200 response body.
  2. Confirm DOLIGENCE_URL points at the actual API service, not an HTML page or proxy intercept.
  3. Bypass any proxy that rewrites HTTPS responses and retry.
  4. 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

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

Related errors


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)