{"record":{"id":"b5eea3437c94f18e","repo":"amir20/dozzle","slug":"token-exchange-failed","errorCode":null,"errorMessage":"token exchange failed","messagePattern":"token exchange failed","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"internal/web/cloud.go","lineNumber":82,"sourceCode":"\t\treturn\n\t}\n\treq.Header.Set(\"User-Agent\", dispatcher.UserAgent)\n\tq := req.URL.Query()\n\tq.Set(\"token\", token)\n\treq.URL.RawQuery = q.Encode()\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"Failed to exchange token\")\n\t\thttp.Error(w, \"failed to exchange token\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))\n\t\tlog.Error().Int(\"status\", resp.StatusCode).Str(\"body\", string(body)).Msg(\"Token exchange failed\")\n\t\thttp.Error(w, \"token exchange failed\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar tokenResp exchangeTokenResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {\n\t\tlog.Error().Err(err).Msg(\"Failed to decode token response\")\n\t\thttp.Error(w, \"failed to decode token response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif tokenResp.Key == \"\" {\n\t\tlog.Error().Msg(\"Empty key received\")\n\t\thttp.Error(w, \"empty key received\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar expiresAt *time.Time\n\tif tokenResp.ExpiresAt != nil {","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/amir20/dozzle/blob/d9463cbe21874e44ab79db6fa63e746ca7d22928/internal/web/cloud.go#L64-L100","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check dozzle server logs for the `Token exchange failed` entry with the actual status and response body.","Restart the cloud connect flow to get a fresh, unused token.","If status is 401/403, verify the token was not modified in transit and the cloud account/API key is valid.","If status is 5xx, wait and retry; the problem is on the cloud service side."],"exampleFix":"// before (log)\n{\"level\":\"error\",\"status\":401,\"body\":\"invalid token\"}\n// after\nrestart cloud connect flow from UI to obtain a fresh token","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"const res = await fetch(exchangeUrl, { method: 'POST' });\nif (!res.ok) {\n  const body = await res.text();\n  console.error('exchange failed', res.status, body); // check dozzle logs for the real status\n}","preventionTips":["Check dozzle server logs for the actual upstream status and body.","Do not reuse old callback URLs; always obtain a fresh token.","Monitor the cloud service status during outages."],"tags":["http","token","upstream"],"backgroundTag":"http-error-response","analyzedSha":"d9463cbe21874e44ab79db6fa63e746ca7d22928","analyzedAt":"2026-09-07T10:08:55.855Z","contentChangedAt":"2026-09-07T10:08:55.855Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}