{"record":{"id":"e09b4882122b36ed","repo":"amir20/dozzle","slug":"failed-to-decode-token-response","errorCode":null,"errorMessage":"failed to decode token response","messagePattern":"failed to decode token response","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"internal/web/cloud.go","lineNumber":89,"sourceCode":"\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 {\n\t\tparsed, err := time.Parse(time.RFC3339, *tokenResp.ExpiresAt)\n\t\tif err != nil {\n\t\t\tlog.Warn().Err(err).Str(\"expiresAt\", *tokenResp.ExpiresAt).Msg(\"Failed to parse expiresAt, ignoring\")\n\t\t} else {\n\t\t\texpiresAt = &parsed\n\t\t}\n\t}","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/amir20/dozzle/blob/d9463cbe21874e44ab79db6fa63e746ca7d22928/internal/web/cloud.go#L71-L107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (raw 200 body)\n<html>login required</html>\n// after\n{\"key\":\"<key>\",\"expiresAt\":\"2026-01-01T00:00:00Z\"}","handlingStrategy":"type-guard","validationCode":"const body = await res.text();\ntry { const j = JSON.parse(body); } catch { throw new Error('exchange response is not JSON: ' + body.slice(0, 200)); }","typeGuard":"function isTokenResponse(v: unknown): v is { key: string; expiresAt?: string } {\n  return typeof v === 'object' && v !== null && typeof (v as any).key === 'string';\n}","tryCatchPattern":"try {\n  const data = JSON.parse(body);\n} catch (e) {\n  // response was HTML/empty; inspect for proxy interference\n}","preventionTips":["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."],"tags":["json","http-response","cloud"],"backgroundTag":"json-decode-failed","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"}