Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion apps/web/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
setUser,
} from "@sentry/nextjs";
import { APICallError, RetryError } from "ai";
import type { z } from "zod";
import { createScopedLogger } from "@/utils/logger";

const logger = createScopedLogger("error");
Expand Down Expand Up @@ -117,6 +116,19 @@ export function isAWSThrottlingError(error: unknown): error is Error {
);
}

export function isOutlookThrottlingError(error: unknown): boolean {
const err = error as Record<string, unknown>;
const code = err?.code as string | undefined;
const statusCode = err?.statusCode as number | undefined;
const message = err?.message as string | undefined;
return (
statusCode === 429 ||
code === "ApplicationThrottled" ||
code === "TooManyRequests" ||
(typeof message === "string" && /MailboxConcurrency/i.test(message))
);
}

export function isAICallError(error: unknown): error is APICallError {
return APICallError.isInstance(error);
}
Expand All @@ -131,6 +143,7 @@ export function isKnownApiError(error: unknown): boolean {
isGmailInsufficientPermissionsError(error) ||
isGmailRateLimitExceededError(error) ||
isGmailQuotaExceededError(error) ||
isOutlookThrottlingError(error) ||
(APICallError.isInstance(error) &&
(isIncorrectOpenAIAPIKeyError(error) ||
isInvalidOpenAIModelError(error) ||
Expand Down Expand Up @@ -174,6 +187,16 @@ export function checkCommonErrors(
};
}

if (isOutlookThrottlingError(error)) {
logger.warn("Outlook throttling error for url", { url });
return {
type: "Outlook Rate Limit",
message:
"Microsoft is temporarily limiting requests. Please try again shortly.",
code: 429,
};
}

if (RetryError.isInstance(error) && isOpenAIRetryError(error)) {
logger.warn("OpenAI quota exceeded for url", { url });
return {
Expand Down
20 changes: 20 additions & 0 deletions apps/web/utils/outlook/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ describe("isRetryableError", () => {
expect(result.retryable).toBe(true);
});

it("identifies ApplicationThrottled code as rate limit", () => {
const errorInfo = {
code: "ApplicationThrottled",
errorMessage: "Application is over its MailboxConcurrency limit.",
};
const result = isRetryableError(errorInfo);
expect(result.isRateLimit).toBe(true);
expect(result.retryable).toBe(true);
});

it("identifies MailboxConcurrency message as rate limit", () => {
const errorInfo = {
status: 429,
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Test doesn't properly isolate message-based detection because status: 429 already triggers rate limit. Use a different status (e.g., 403) or omit status to ensure the test validates the MailboxConcurrency regex matching.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/outlook/retry.test.ts, line 83:

<comment>Test doesn&#39;t properly isolate message-based detection because `status: 429` already triggers rate limit. Use a different status (e.g., 403) or omit status to ensure the test validates the MailboxConcurrency regex matching.</comment>

<file context>
@@ -68,6 +68,26 @@ describe(&quot;isRetryableError&quot;, () =&gt; {
+
+  it(&quot;identifies MailboxConcurrency message as rate limit&quot;, () =&gt; {
+    const errorInfo = {
+      status: 429,
+      errorMessage: &quot;MailboxConcurrency limit exceeded&quot;,
+    };
</file context>
Suggested change
status: 429,
status: 403,
Fix with Cubic

errorMessage: "MailboxConcurrency limit exceeded",
};
const result = isRetryableError(errorInfo);
expect(result.isRateLimit).toBe(true);
expect(result.retryable).toBe(true);
});

it("identifies server errors", () => {
for (const status of [502, 503, 504]) {
const errorInfo = { status, errorMessage: "Server error" };
Expand Down
7 changes: 4 additions & 3 deletions apps/web/utils/outlook/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface ErrorInfo {

/**
* Retries a Microsoft Graph API operation when rate limits or temporary server errors are encountered
* - Rate limits: 429, "TooManyRequests" code
* - Rate limits: 429, "TooManyRequests", "ApplicationThrottled", "MailboxConcurrency"
* - Server errors: 502, 503, 504, "ServiceNotAvailable", "ServerBusy"
*/
export async function withOutlookRetry<T>(
Expand Down Expand Up @@ -121,12 +121,13 @@ export function isRetryableError(errorInfo: ErrorInfo): {
} {
const { status, code, errorMessage } = errorInfo;

// Rate limit detection: 429 status or "TooManyRequests" code
// Rate limit detection: 429 status, throttling codes, or rate limit messages
const isRateLimit =
status === 429 ||
code === "TooManyRequests" ||
code === "ApplicationThrottled" ||
/rate limit/i.test(errorMessage) ||
/quota exceeded/i.test(errorMessage);
/MailboxConcurrency/i.test(errorMessage);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Temporary server errors that should be retried (502, 503, 504)
const isServerError =
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.23.3
v2.23.4
Loading