gastownhall/beads · error

oauth: failed to create token request: %w

Error message

oauth: failed to create token request: %w

What it means

Wraps http.NewRequest failures during OAuth token acquisition. The token request URL or body could not be constructed, so no HTTP call was attempted. Usually indicates a malformed TokenURL (bad scheme/control characters) rather than a transient issue.

Source

Thrown at internal/linear/oauth.go:125

	defer m.mu.Unlock()
	m.token = ""
	m.expiresAt = time.Time{}
	debug.Logf("oauth: token invalidated, will re-acquire on next request")
}

// acquireToken performs the client_credentials grant. Caller must hold m.mu write lock.
func (m *OAuthTokenManager) acquireToken() error {
	data := url.Values{
		"grant_type":    {"client_credentials"},
		"client_id":     {m.config.ClientID},
		"client_secret": {m.config.ClientSecret},
		"scope":         {m.config.Scopes},
		"actor":         {m.config.Actor},
	}

	req, err := http.NewRequest("POST", m.config.TokenURL, strings.NewReader(data.Encode()))
	if err != nil {
		return fmt.Errorf("oauth: failed to create token request: %w", err)
	}
	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)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Validate m.config.TokenURL at startup (url.Parse must succeed and scheme must be http/https).
  2. Log the TokenURL (redacted) from the wrapped error context to spot encoding artifacts.
  3. Trim whitespace/newlines from the token URL when loading config from env or files.

Example fix

// before
TokenURL: os.Getenv("LINEAR_TOKEN_URL")
// after
tokenURL := strings.TrimSpace(os.Getenv("LINEAR_TOKEN_URL"))
if u, err := url.Parse(tokenURL); err != nil || (u.Scheme != "http" && u.Scheme != "https") {
    return nil, fmt.Errorf("invalid LINEAR_TOKEN_URL %q: %w", tokenURL, err)
}
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(cfg.TokenURL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
    return fmt.Errorf("invalid token URL %q", cfg.TokenURL)
}

Prevention

When it happens

Trigger: acquireToken (called via Token) builds a POST to m.config.TokenURL; http.NewRequest returns an error for an unparsable URL or invalid method.

Common situations: TokenURL set to an empty string or a value with spaces/newlines from a misparsed env var; config built with url.QueryEscape applied twice; typo like 'htp://' in configuration.

Related errors


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