{"record":{"id":"0e7a6c4bcb9d01fa","repo":"netbirdio/netbird","slug":"parsing-token-response-failed-with-error-v","errorCode":null,"errorMessage":"parsing token response failed with error: %v","messagePattern":"parsing token response failed with error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/internal/auth/device_flow.go","lineNumber":243,"sourceCode":"\t\terr := res.Body.Close()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn TokenRequestResponse{}, fmt.Errorf(\"failed reading access token response body with error: %v\", err)\n\t}\n\n\tif res.StatusCode > 499 {\n\t\treturn TokenRequestResponse{}, fmt.Errorf(\"access token response returned code: %s\", string(body))\n\t}\n\n\ttokenResponse := TokenRequestResponse{}\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn TokenRequestResponse{}, fmt.Errorf(\"parsing token response failed with error: %v\", err)\n\t}\n\n\treturn tokenResponse, nil\n}\n\n// WaitToken waits user's login and authorize the app. Once the user's authorize\n// it retrieves the access token from Hosted's endpoint and validates it before returning.\n// The method creates a timeout context internally based on info.ExpiresIn.\nfunc (d *DeviceAuthorizationFlow) WaitToken(ctx context.Context, info AuthFlowInfo) (TokenInfo, error) {\n\t// Create timeout context based on flow expiration\n\ttimeout := time.Duration(info.ExpiresIn) * time.Second\n\twaitCtx, cancel := context.WithTimeout(ctx, timeout)\n\tdefer cancel()\n\n\tinterval := time.Duration(info.Interval) * time.Second\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/internal/auth/device_flow.go#L225-L261","documentation":"Raised by DeviceAuthorizationFlow.requestToken when json.Unmarshal fails on the token-endpoint response body (device_flow.go:240-244). The response had a status of 499 or below, so the code assumed a JSON OAuth payload, but the body is not valid JSON (or does not fit TokenRequestResponse). This almost always means something other than the IdP answered: an HTML error page, a captive portal, or an empty body.","triggerScenarios":"Token endpoint returns 200/4xx with HTML instead of JSON: captive-portal login pages on guest Wi-Fi, proxy block pages, WAF interstitials, or a token endpoint URL that actually points to a website. Also produced by empty bodies from some load balancers on 4xx.","commonSituations":"Hotel/airport Wi-Fi captive portals intercepting HTTPS-less flows, corporate WAF rewriting IdP responses, a management configuration where TokenEndpoint was typo'd or left as the IdP's generic domain rather than the /oauth/token path, expired device code answered with non-JSON by non-conformant IdPs.","solutions":["Log or capture the raw body (temporarily wrap the read) to see what actually came back - HTML reveals a portal/proxy, garbage reveals a wrong URL","Verify the TokenEndpoint URL configured for the IdP on the NetBird management side resolves to the real OAuth token endpoint (e.g. https://tenant.eu.auth0.com/oauth/token)","Complete the captive portal login or move to an unfiltered network and retry","If a proxy is in path, exempt the IdP domain from content rewriting"],"exampleFix":"// debugging aid: include a body preview in the error\nerr = json.Unmarshal(body, &tokenResponse)\nif err != nil {\n\tpreview := string(body)\n\tif len(preview) > 200 {\n\t\tpreview = preview[:200]\n\t}\n\treturn TokenRequestResponse{}, fmt.Errorf(\"parsing token response failed with error: %v, body: %s\", err, preview)\n}","handlingStrategy":"try-catch","validationCode":"// Confirm the endpoint answers JSON before starting the flow\nfunc endpointServesJSON(endpoint string) error {\n\tres, err := http.Post(endpoint, \"application/x-www-form-urlencoded\", strings.NewReader(\"client_id=x\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tbody, _ := io.ReadAll(io.LimitReader(res.Body, 512))\n\tvar probe map[string]any\n\tif json.Unmarshal(body, &probe) != nil && !strings.HasPrefix(res.Header.Get(\"Content-Type\"), \"application/json\") {\n\t\treturn fmt.Errorf(\"endpoint returned non-JSON (%s): %s\", res.Header.Get(\"Content-Type\"), body)\n\t}\n\treturn nil\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n\tvar syn *json.SyntaxError\n\tif errors.As(unwrapToCause(err), &syn) {\n\t\t// body was not JSON: suspect captive portal, proxy block page, or wrong TokenEndpoint\n\t}\n}","preventionTips":["Verify the TokenEndpoint URL in the management IdP config is the real OAuth token endpoint, not a website root","Complete captive portals or bypass filtering middleboxes before interactive login","Capture the raw body when this fires - the content instantly identifies the interceptor"],"tags":["oauth2","device-flow","json","idp","captive-portal","proxy"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}