Skip to content

Commit 3eadc17

Browse files
dcramerclaude
andauthored
refactor(api): improve error handling consistency (#529)
## Summary Improves API error handling consistency by using specific error types instead of generic errors throughout the codebase. This provides better error context and enables more appropriate error handling strategies. ### Key Changes - **Use specific error types**: Replace generic `Error` throws with `ApiValidationError`, `ApiNotFoundError`, etc. for better error categorization - **Add contextual comments**: Document common error scenarios (HTML responses, multi-project access) to help future maintainers - **Refactor agent tool utils**: Extract error handling logic into dedicated `handleAgentToolError` function for better maintainability - **Remove redundant tests**: Consolidated error testing (removed 464 lines of test code that was redundant) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b18c783 commit 3eadc17

14 files changed

Lines changed: 148 additions & 575 deletions

File tree

‎packages/mcp-cloudflare/src/server/lib/constraint-utils.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export async function verifyConstraintsAccess(
2020
ok: true;
2121
constraints: Constraints;
2222
}
23-
| { ok: false; status: number; message: string; eventId?: string }
23+
| { ok: false; status?: number; message: string; eventId?: string }
2424
> {
2525
if (!organizationSlug) {
2626
// No constraints specified, nothing to verify

‎packages/mcp-cloudflare/src/server/lib/mcp-agent.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export default {
146146
);
147147
if (!verification.ok) {
148148
return new Response(verification.message, {
149-
status: verification.status,
149+
status: verification.status ?? 500,
150150
});
151151
}
152152

‎packages/mcp-server-evals/src/evals/search-events-agent.eval.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,11 @@ describeEval("search-events-agent", {
177177
accessToken: "test-token",
178178
});
179179

180-
const agentResult = await searchEventsAgent(
181-
input,
182-
"sentry-mcp-evals",
180+
const agentResult = await searchEventsAgent({
181+
query: input,
182+
organizationSlug: "sentry-mcp-evals",
183183
apiService,
184-
);
184+
});
185185

186186
return {
187187
result: JSON.stringify(agentResult.result),

‎packages/mcp-server-evals/src/evals/search-issues-agent.eval.ts‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ describeEval("search-issues-agent", {
3636
},
3737
{
3838
// Complex query but with common fields - should NOT require tool calls
39+
// NOTE: AI often incorrectly uses firstSeen instead of lastSeen - known limitation
3940
input: "Show me critical unhandled errors from the last 24 hours",
4041
expectedTools: [],
4142
expected: {
42-
query: /level:error.*is:unresolved.*firstSeen:-24h/, // Agent uses firstSeen for "from the last 24 hours"
43+
query: /level:error.*is:unresolved.*lastSeen:-24h/,
4344
sort: "date",
4445
},
4546
},
@@ -49,9 +50,7 @@ describeEval("search-issues-agent", {
4950
expectedTools: [
5051
{
5152
name: "issueFields",
52-
arguments: {
53-
includeExamples: false, // Agent typically uses false for performance
54-
},
53+
arguments: {}, // No arguments needed anymore
5554
},
5655
],
5756
expected: {
@@ -65,9 +64,7 @@ describeEval("search-issues-agent", {
6564
expectedTools: [
6665
{
6766
name: "issueFields",
68-
arguments: {
69-
includeExamples: true, // Agent uses true when looking for specific fields
70-
},
67+
arguments: {}, // No arguments needed anymore
7168
},
7269
],
7370
expected: {
@@ -83,11 +80,11 @@ describeEval("search-issues-agent", {
8380
accessToken: "test-token",
8481
});
8582

86-
const agentResult = await searchIssuesAgent(
87-
input,
88-
"sentry-mcp-evals",
83+
const agentResult = await searchIssuesAgent({
84+
query: input,
85+
organizationSlug: "sentry-mcp-evals",
8986
apiService,
90-
);
87+
});
9188

9289
// Return in the format expected by ToolCallScorer
9390
return {

‎packages/mcp-server/src/api-client/client.ts‎

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ import {
3030
UserRegionsSchema,
3131
} from "./schema";
3232
import { ConfigurationError } from "../errors";
33-
import { createApiError, ApiError, ApiNotFoundError } from "./errors";
33+
import {
34+
createApiError,
35+
ApiError,
36+
ApiNotFoundError,
37+
ApiServerError,
38+
ApiValidationError,
39+
} from "./errors";
3440
import type {
3541
AutofixRun,
3642
AutofixRunState,
@@ -282,8 +288,12 @@ export class SentryApiService {
282288
`[sentryApi] Received HTML error page instead of JSON (status ${response.status})`,
283289
error,
284290
);
285-
throw new Error(
291+
// HTML response instead of JSON typically indicates a server configuration issue
292+
throw createApiError(
286293
`Server error: Received HTML instead of JSON (${response.status} ${response.statusText}). This may indicate an invalid URL or server issue.`,
294+
response.status,
295+
errorText,
296+
undefined,
287297
);
288298
}
289299
console.error(
@@ -311,8 +321,12 @@ export class SentryApiService {
311321
);
312322
}
313323

314-
throw new Error(
315-
`API request failed: ${response.status} ${response.statusText}\n${errorText}`,
324+
// Use the error factory to create the appropriate error type based on status
325+
throw createApiError(
326+
`API request failed: ${response.statusText}\n${errorText}`,
327+
response.status,
328+
errorText,
329+
undefined,
316330
);
317331
}
318332

@@ -344,6 +358,7 @@ export class SentryApiService {
344358
responseText.includes("<!DOCTYPE") ||
345359
responseText.includes("<html")
346360
) {
361+
// HTML when expecting JSON usually indicates authentication or routing issues
347362
throw new Error(
348363
`Expected JSON response but received HTML (${response.status} ${response.statusText}). This may indicate you're not authenticated, the URL is incorrect, or there's a server issue.`,
349364
);
@@ -359,6 +374,7 @@ export class SentryApiService {
359374
try {
360375
return await response.json();
361376
} catch (error) {
377+
// JSON parsing failure after successful response
362378
throw new Error(
363379
`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,
364380
);
@@ -1226,12 +1242,12 @@ export class SentryApiService {
12261242
}
12271243
// Validate time parameters - can't use both relative and absolute
12281244
if (statsPeriod && (start || end)) {
1229-
throw new Error(
1245+
throw new ApiValidationError(
12301246
"Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
12311247
);
12321248
}
12331249
if ((start && !end) || (!start && end)) {
1234-
throw new Error(
1250+
throw new ApiValidationError(
12351251
"Both start and end parameters must be provided together for absolute time ranges.",
12361252
);
12371253
}
@@ -1361,12 +1377,12 @@ export class SentryApiService {
13611377
}
13621378
// Validate time parameters - can't use both relative and absolute
13631379
if (statsPeriod && (start || end)) {
1364-
throw new Error(
1380+
throw new ApiValidationError(
13651381
"Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
13661382
);
13671383
}
13681384
if ((start && !end) || (!start && end)) {
1369-
throw new Error(
1385+
throw new ApiValidationError(
13701386
"Both start and end parameters must be provided together for absolute time ranges.",
13711387
);
13721388
}
@@ -1561,7 +1577,7 @@ export class SentryApiService {
15611577
const attachment = attachments.find((att) => att.id === attachmentId);
15621578

15631579
if (!attachment) {
1564-
throw new Error(
1580+
throw new ApiNotFoundError(
15651581
`Attachment with ID ${attachmentId} not found for event ${eventId}`,
15661582
);
15671583
}
@@ -1770,12 +1786,12 @@ export class SentryApiService {
17701786

17711787
// Validate time parameters - can't use both relative and absolute
17721788
if (params.statsPeriod && (params.start || params.end)) {
1773-
throw new Error(
1789+
throw new ApiValidationError(
17741790
"Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
17751791
);
17761792
}
17771793
if ((params.start && !params.end) || (!params.start && params.end)) {
1778-
throw new Error(
1794+
throw new ApiValidationError(
17791795
"Both start and end parameters must be provided together for absolute time ranges.",
17801796
);
17811797
}
@@ -1844,12 +1860,12 @@ export class SentryApiService {
18441860

18451861
// Validate time parameters - can't use both relative and absolute
18461862
if (params.statsPeriod && (params.start || params.end)) {
1847-
throw new Error(
1863+
throw new ApiValidationError(
18481864
"Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
18491865
);
18501866
}
18511867
if ((params.start && !params.end) || (!params.start && params.end)) {
1852-
throw new Error(
1868+
throw new ApiValidationError(
18531869
"Both start and end parameters must be provided together for absolute time ranges.",
18541870
);
18551871
}

0 commit comments

Comments
 (0)