netbirdio/netbird · error
parsing token response failed with error: %v
Error message
parsing token response failed with error: %v
What it means
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.
Source
Thrown at client/internal/auth/device_flow.go:243
err := res.Body.Close()
if err != nil {
return
}
}()
body, err := io.ReadAll(res.Body)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("failed reading access token response body with error: %v", err)
}
if res.StatusCode > 499 {
return TokenRequestResponse{}, fmt.Errorf("access token response returned code: %s", string(body))
}
tokenResponse := TokenRequestResponse{}
err = json.Unmarshal(body, &tokenResponse)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v", err)
}
return tokenResponse, nil
}
// WaitToken waits user's login and authorize the app. Once the user's authorize
// it retrieves the access token from Hosted's endpoint and validates it before returning.
// The method creates a timeout context internally based on info.ExpiresIn.
func (d *DeviceAuthorizationFlow) WaitToken(ctx context.Context, info AuthFlowInfo) (TokenInfo, error) {
// Create timeout context based on flow expiration
timeout := time.Duration(info.ExpiresIn) * time.Second
waitCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
interval := time.Duration(info.Interval) * time.Second
ticker := time.NewTicker(interval)
defer ticker.Stop()
View on GitHub (pinned to 93e97f4bf1)
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
Example fix
// debugging aid: include a body preview in the error
err = json.Unmarshal(body, &tokenResponse)
if err != nil {
preview := string(body)
if len(preview) > 200 {
preview = preview[:200]
}
return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v, body: %s", err, preview)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Confirm the endpoint answers JSON before starting the flow
func endpointServesJSON(endpoint string) error {
res, err := http.Post(endpoint, "application/x-www-form-urlencoded", strings.NewReader("client_id=x"))
if err != nil {
return err
}
defer res.Body.Close()
body, _ := io.ReadAll(io.LimitReader(res.Body, 512))
var probe map[string]any
if json.Unmarshal(body, &probe) != nil && !strings.HasPrefix(res.Header.Get("Content-Type"), "application/json") {
return fmt.Errorf("endpoint returned non-JSON (%s): %s", res.Header.Get("Content-Type"), body)
}
return nil
} Try / catch
if err != nil {
var syn *json.SyntaxError
if errors.As(unwrapToCause(err), &syn) {
// body was not JSON: suspect captive portal, proxy block page, or wrong TokenEndpoint
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- failed reading access token response body with error: %v
- access token response returned code: %s
- validate access token failed with error: %v
- PKCE authorization flow failed: %v
- authentication failed: %s
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/0e7a6c4bcb9d01fa.
Report an issue: GitHub.