charmbracelet/crush · error

failed to refresh OAuth token: status code %d

Error message

failed to refresh OAuth token: status code %d

What it means

RefreshOAuthToken expects HTTP 200 from /workspaces/{id}/config/refresh-oauth. Any other status (4xx/5xx) produces this error. It means the server received the request but refused or failed it — unlike error 270, the network round-trip succeeded.

Source

Thrown at internal/client/config.go:160

	if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
		return nil, false, fmt.Errorf("failed to decode import copilot response: %w", err)
	}
	return result.Token, result.Success, nil
}

// RefreshOAuthToken refreshes an OAuth token for a provider on the
// server.
func (c *Client) RefreshOAuthToken(ctx context.Context, id string, scope config.Scope, providerID string) error {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/refresh-oauth", id), nil, jsonBody(struct {
		Scope      config.Scope `json:"scope"`
		ProviderID string       `json:"provider_id"`
	}{Scope: scope, ProviderID: providerID}), http.Header{"Content-Type": []string{"application/json"}})
	if err != nil {
		return fmt.Errorf("failed to refresh OAuth token: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to refresh OAuth token: status code %d", rsp.StatusCode)
	}
	return nil
}

// ProjectNeedsInitialization checks if the project needs
// initialization.
func (c *Client) ProjectNeedsInitialization(ctx context.Context, id string) (bool, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/project/needs-init", id), nil, nil)
	if err != nil {
		return false, fmt.Errorf("failed to check project init: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return false, fmt.Errorf("failed to check project init: status code %d", rsp.StatusCode)
	}
	var result struct {
		NeedsInit bool `json:"needs_init"`
	}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Log rsp.StatusCode variants: re-authenticate the provider interactively if 401/403 (refresh grant is dead)
  2. Verify the workspace id and providerID are correct if you see 404
  3. Check server logs for 5xx responses; this is a server-side token-exchange failure
  4. Retry with backoff only for 429/5xx; do not retry 4xx client errors

Example fix

// before
if err := client.RefreshOAuthToken(ctx, id, scope, providerID); err != nil {
    return err
}
// after
if err := client.RefreshOAuthToken(ctx, id, scope, providerID); err != nil {
    if strings.Contains(err.Error(), "status code 401") {
        return requireReauthentication(providerID)
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the provider is registered in the workspace before refreshing
providers, err := client.ListProviders(ctx, id)
if err != nil {
    return err
}
found := false
for _, p := range providers {
    if p.ID == providerID {
        found = true
    }
}
if !found {
    return fmt.Errorf("provider %q not in workspace %s", providerID, id)
}

Type guard

func isStatusError(err error, code int) bool {
    return err != nil && strings.Contains(err.Error(), fmt.Sprintf("status code %d", code))
}

Try / catch

if err := c.RefreshOAuthToken(ctx, id, scope, pid); err != nil {
    if isStatusError(err, 401) || isStatusError(err, 403) {
        return triggerInteractiveLogin(pid)
    }
    return err
}

Prevention

When it happens

Trigger: Calling RefreshOAuthToken and the server responds with a non-200 status, e.g. 401 when the refresh grant itself is invalid, 404 for an unknown workspace id, or 500 on a server-side provider token exchange failure.

Common situations: The stored refresh token was revoked or expired beyond the grace window; the providerID no longer exists in the workspace config; the caller lacks scope permission for the workspace; server bug during provider token rotation.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/e9c91fcba6342013. Report an issue: GitHub.