Skip to content

security: no HTTP 429 rate-limit handling — silent failures when ClickUp throttles requests #8

@SippieCup

Description

@SippieCup

Severity

Medium

Description

The plugin has no handling for HTTP 429 (Too Many Requests) responses from the ClickUp API. A rate-limited response falls through to the generic error handler — meaning the release pipeline fails with an unhelpful message rather than surfacing the actual cause.

Additionally, the automatic fallback retry from "post" to "message" type (triggered on any HTTP 400) fires a second API request without checking whether the first request was rate-limited.

Vulnerable codesrc/send-message.ts:30-43:

if (isRichType(type) && shouldFallbackToMessage(response)) {
  // This fires on ANY 400, including rate-limit adjacent errors
  response = await fetch(url, { ... });
}

// shouldFallbackToMessage only checks for 400:
function shouldFallbackToMessage(response: Response): boolean {
  return response.status === 400;
}

Risk

  • A 429 response causes the release pipeline to fail with a generic "HTTP 429" error — no guidance on when to retry
  • The Retry-After header is never read, so the user/pipeline operator has no way to know the retry window
  • In CI pipelines with automatic reruns, this can create a retry storm against the ClickUp API

Recommended Fix

Detect 429 responses and include Retry-After guidance in the error:

if (response.status === 429) {
  const retryAfter = response.headers.get("retry-after") ?? "unknown";
  throw getError("ERATELIMITED", retryAfter);
}

Add to src/errors.ts:

ERATELIMITED: (retryAfter: string) => ({
  message: "ClickUp API rate limit exceeded.",
  details: `The ClickUp API returned HTTP 429. Retry after: ${retryAfter} seconds.`,
}),

AI Fix Prompt

In src/send-message.ts and src/errors.ts, add explicit handling for HTTP 429 (rate limit) responses from the ClickUp API.

Changes needed:
1. In src/errors.ts, add a new ERATELIMITED error code. It takes one argument (retryAfter: string) and returns a message "ClickUp API rate limit exceeded." with details explaining the retry-after value.
2. In src/send-message.ts, after each fetch() call (both the initial and the fallback), check if response.status === 429 before the generic !response.ok check. If 429, read the "retry-after" header (default to "unknown" if absent), and throw getError("ERATELIMITED", retryAfter).
3. Also ensure the fallback retry logic (shouldFallbackToMessage) only fires on 400 status, not 429. The current implementation already does this, but confirm with a comment.
4. Add a test in test/send-message.test.ts that mocks a 429 response and verifies the ERATELIMITED error is thrown with the retry-after value from the response header.
5. Keep changes minimal — do not add automatic retry or backoff logic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity vulnerability or concern

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions