gastownhall/beads · error

oauth: token request returned status %d: %s

Error message

oauth: token request returned status %d: %s

What it means

The token endpoint returned a non-200 status whose body was not a recognizable OAuth error JSON (no error field), so the raw status code and body are surfaced verbatim for diagnosis.

Source

Thrown at internal/linear/oauth.go:145

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := m.client.Do(req)
	if err != nil {
		return fmt.Errorf("oauth: token request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit
	if err != nil {
		return fmt.Errorf("oauth: failed to read token response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		var errResp oauthErrorResponse
		if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" {
			return fmt.Errorf("oauth: token request failed (%s): %s", errResp.Error, errResp.Description)
		}
		return fmt.Errorf("oauth: token request returned status %d: %s", resp.StatusCode, string(body))
	}

	var tokenResp oauthTokenResponse
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return fmt.Errorf("oauth: failed to parse token response: %w", err)
	}

	if tokenResp.AccessToken == "" {
		return fmt.Errorf("oauth: token response missing access_token")
	}

	m.token = tokenResp.AccessToken
	m.expiresAt = m.nowFunc().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)

	debug.Logf("oauth: acquired token (expires in %ds)", tokenResp.ExpiresIn)
	return nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the returned body in the error: HTML content usually means the TokenURL is wrong or a proxy is intercepting.
  2. Verify TokenURL points at the actual token endpoint (e.g. https://api.linear.app/oauth/token).
  3. Check for proxy/WAF blocking (502/503/403 with HTML) and allowlist the endpoint.
  4. Retry with backoff if the status is 5xx (transient upstream outage).

Example fix

// before
TokenURL: "https://linear.app/oauth/token" // marketing site, not API
// after
TokenURL: "https://api.linear.app/oauth/token"
Defensive patterns

Strategy: try-catch

Validate before calling

// Probe the endpoint once at startup
tokURL := cfg.TokenURL
if resp, err := http.Post(tokURL, "application/x-www-form-urlencoded", nil); err == nil && resp.StatusCode >= 500 {
    log.Printf("warning: token endpoint unhealthy: %d", resp.StatusCode)
}

Try / catch

var statusErr *HTTPStatusError
if errors.As(err, &statusErr) && statusErr.StatusCode >= 500 {
    // transient gateway failure — retry with backoff
}

Prevention

When it happens

Trigger: resp.StatusCode != 200 and either json.Unmarshal into oauthErrorResponse fails or errResp.Error is empty in acquireToken.

Common situations: Reverse proxy returning HTML 502/503 error pages; Cloudflare challenges; wrong TokenURL hitting a login page (302 followed to HTML); rate-limit pages without OAuth JSON.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/7134fd2059883341. Report an issue: GitHub.