amir20/dozzle · error

token exchange failed

Error message

token exchange failed

What it means

cloudCallback requires the token-exchange endpoint to answer HTTP 200. Any other status (4xx/5xx from the cloud service) is turned into a 500 with this generic message; the real status and first 1KB of the response body are only visible in dozzle's server logs under `Token exchange failed`.

Solutions

  1. Check dozzle server logs for the `Token exchange failed` entry with the actual status and response body.
  2. Restart the cloud connect flow to get a fresh, unused token.
  3. If status is 401/403, verify the token was not modified in transit and the cloud account/API key is valid.
  4. If status is 5xx, wait and retry; the problem is on the cloud service side.

Example fix

// before (log)
{"level":"error","status":401,"body":"invalid token"}
// after
restart cloud connect flow from UI to obtain a fresh token
Defensive patterns

Strategy: fallback

Try / catch

const res = await fetch(exchangeUrl, { method: 'POST' });
if (!res.ok) {
  const body = await res.text();
  console.error('exchange failed', res.status, body); // check dozzle logs for the real status
}

Prevention

When it happens

Trigger: The POST to /api/exchange-token returns a non-200 status, e.g. 401/403 for an invalid or expired callback token, 410 for a used/stale token, or 5xx from the cloud service itself.

Common situations: Replaying an old callback URL whose one-time token was already consumed; tampered or expired token in the redirect; cloud service outage; clock skew invalidating a short-lived token.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07). Data as JSON: /api/errors/b5eea3437c94f18e. Report an issue: GitHub.

Appendix: source

Thrown at internal/web/cloud.go:82

		return
	}
	req.Header.Set("User-Agent", dispatcher.UserAgent)
	q := req.URL.Query()
	q.Set("token", token)
	req.URL.RawQuery = q.Encode()

	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 {

View on GitHub (pinned to d9463cbe21)