From e12aa02a83684d858bcabceb40ce558a4de96d27 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:46:28 +0000 Subject: [PATCH] fix(event): Prevent immediate retry of rate-limited org in event view fallback --- packages/cli/src/commands/event/view.ts | 17 ++++++++++---- packages/cli/test/commands/event/view.test.ts | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/event/view.ts b/packages/cli/src/commands/event/view.ts index d54b5f6a0..c43091b56 100644 --- a/packages/cli/src/commands/event/view.ts +++ b/packages/cli/src/commands/event/view.ts @@ -703,6 +703,10 @@ async function tryEventFallbacks( // Track whether the search completed so we can skip the org in cross-org // only when we got a definitive "not found" (not a transient failure). let sameOrgSearched = false; + // Track rate-limit separately: a 429 means the org is reachable but throttled. + // Retrying it immediately in the cross-org fallback would just hit the same + // limit again, producing consecutive identical HTTP requests (CLI-2Y1). + let orgRateLimited = false; try { const resolved = await resolveEventInOrg(org, eventId); sameOrgSearched = true; @@ -717,15 +721,20 @@ async function tryEventFallbacks( if (sameOrgError instanceof AuthError) { throw sameOrgError; } - // Transient failure — don't mark org as searched so cross-org retries it + if (sameOrgError instanceof ApiError && sameOrgError.status === 429) { + // Rate-limited — exclude the org from cross-org search; an immediate + // retry against the same endpoint would hit the same limit. + orgRateLimited = true; + } + logger.debug("Same-org event lookup failed", sameOrgError); } // Cross-org fallback: the event may exist in a different organization. - // Only exclude the org if the same-org search completed successfully - // (returned null). If it threw a transient error, let cross-org retry it. + // Exclude the org when the same-org search completed (returned null) OR + // when it was rate-limited — either way, re-querying it immediately is futile. try { const crossOrg = await findEventAcrossOrgs(eventId, { - excludeOrgs: sameOrgSearched ? [org] : undefined, + excludeOrgs: sameOrgSearched || orgRateLimited ? [org] : undefined, }); if (crossOrg) { // Use project-scoped phrasing when found in same org (different project) diff --git a/packages/cli/test/commands/event/view.test.ts b/packages/cli/test/commands/event/view.test.ts index 6a8aa2bf3..850875413 100644 --- a/packages/cli/test/commands/event/view.test.ts +++ b/packages/cli/test/commands/event/view.test.ts @@ -1370,6 +1370,29 @@ describe("fetchEventWithContext", () => { }); }); + test("cross-org excludes org when same-org search was rate-limited (429)", async () => { + vi.spyOn(apiClient, "getEvent").mockRejectedValue( + new ApiError("Not found", 404) + ); + // Same-org search hit a rate limit — retrying it immediately is futile (CLI-2Y1) + vi.spyOn(apiClient, "resolveEventInOrg").mockRejectedValue( + new ApiError("Too Many Requests", 429) + ); + const findSpy = vi + .spyOn(apiClient, "findEventAcrossOrgs") + .mockResolvedValue(null); + + await expect( + fetchEventWithContext(null, "my-org", "my-project", "abc123") + ).rejects.toThrow(ResolutionError); + + // org must be excluded — re-querying a rate-limited endpoint immediately + // produces consecutive identical HTTP requests (the Consecutive HTTP issue). + expect(findSpy).toHaveBeenCalledWith("abc123", { + excludeOrgs: ["my-org"], + }); + }); + test("swallows non-auth cross-org errors and throws ResolutionError", async () => { vi.spyOn(apiClient, "getEvent").mockRejectedValue( new ApiError("Not found", 404)