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

  1. 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
  2. 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)
  3. Complete the captive portal login or move to an unfiltered network and retry
  4. 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

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


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/0e7a6c4bcb9d01fa. Report an issue: GitHub.