Skip to content

Commit b280a8f

Browse files
committed
feat(client): enhance error handling with standardized error codes
BREAKING CHANGE: API error responses now use standard HTTP status codes - Add error_code field to ApiError interface for granular error handling - Add 402 Payment Required handler for PRO features and subscriptions - Enhance 401 handler with specific error codes (TOKEN_EXPIRED, MFA_REQUIRED) - Enhance 429 handler with rate limit details (retry_after, plan limits) - Remove deprecated 400/422 handlers (now consolidated to 409 Conflict) Error codes follow RFC 9110 HTTP Semantics: - 401: Unauthorized (TOKEN_EXPIRED, TOKEN_INVALID, MFA_REQUIRED) - 402: Payment Required (PRO_FEATURE_REQUIRED, SUBSCRIPTION_EXPIRED) - 403: Forbidden (PERMISSION_DENIED) - 404: Not Found (RESOURCE_NOT_FOUND) - 409: Conflict (VALIDATION_FAILED, CONFLICT) - 429: Too Many Requests (RATE_LIMIT_EXCEEDED) - 500: Server Error Also updates contact email to support@gitscrum.com
1 parent f94d65d commit b280a8f

2 files changed

Lines changed: 82 additions & 27 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
],
5555
"author": {
5656
"name": "GitScrum",
57-
"email": "hello@gitscrum.com",
57+
"email": "support@gitscrum.com",
5858
"url": "https://gitscrum.com"
5959
},
6060
"license": "MIT",

src/client/GitScrumClient.ts

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* - Full TypeScript support
1313
*
1414
* @module @gitscrum-studio/mcp-server/client
15-
* @author GitScrum <hello@gitscrum.com>
15+
* @author GitScrum <support@gitscrum.com>
1616
* @license MIT
1717
*/
1818

@@ -29,9 +29,27 @@ export interface ApiResponse<T> {
2929
};
3030
}
3131

32+
/**
33+
* Standard API error response from GitScrum API
34+
* All errors follow this format with HTTP status codes:
35+
* - 401: Unauthorized (token issues)
36+
* - 402: Payment Required (PRO features)
37+
* - 403: Forbidden (permission denied)
38+
* - 404: Not Found
39+
* - 409: Conflict (validation, business rules)
40+
* - 429: Rate Limited
41+
* - 500: Server Error
42+
*/
3243
interface ApiError {
44+
error_code?: string;
3345
message: string;
3446
errors?: Record<string, string[]>;
47+
// 402 specific
48+
feature?: string;
49+
upgrade_url?: string;
50+
// 429 specific
51+
retry_after?: number;
52+
plan?: string;
3553
}
3654

3755
export class GitScrumClient {
@@ -102,56 +120,93 @@ export class GitScrumClient {
102120

103121
if (!response.ok) {
104122
const errorData = (await response.json().catch(() => ({}))) as ApiError;
123+
const errorCode = errorData.error_code || "";
105124
const errorMessage = errorData.message || `API Error: ${response.status}`;
106125

107-
// Handle 400 Bad Request - validation or invalid input
108-
if (response.status === 400) {
109-
throw new Error(`Invalid request: ${errorMessage}`);
110-
}
111-
112126
// Handle 401 Unauthorized - authentication issue
113127
if (response.status === 401) {
114-
throw new Error("Session expired. Authentication required. Use login to reconnect.");
128+
// Use error_code for specific auth errors
129+
if (errorCode === "TOKEN_EXPIRED" || errorCode === "SESSION_EXPIRED") {
130+
throw new Error("Session expired. Use auth_login to reconnect.");
131+
}
132+
if (errorCode === "TOKEN_INVALID" || errorCode === "TOKEN_MISSING") {
133+
throw new Error("Authentication required. Use auth_login to authenticate.");
134+
}
135+
if (errorCode === "MFA_REQUIRED") {
136+
throw new Error("Multi-factor authentication required.");
137+
}
138+
throw new Error("Authentication required. Use auth_login to authenticate.");
139+
}
140+
141+
// Handle 402 Payment Required - PRO feature or subscription issue
142+
if (response.status === 402) {
143+
const feature = errorData.feature || "this feature";
144+
if (errorCode === "PRO_FEATURE_REQUIRED") {
145+
throw new Error(`PRO feature required: ${feature}. Upgrade at ${errorData.upgrade_url || "https://gitscrum.com/pricing"}`);
146+
}
147+
if (errorCode === "SUBSCRIPTION_EXPIRED") {
148+
throw new Error(`Subscription expired. Renew at ${errorData.upgrade_url || "https://gitscrum.com/pricing"}`);
149+
}
150+
throw new Error(`Payment required: ${errorMessage}`);
115151
}
116152

117-
// Handle 403 Forbidden
153+
// Handle 403 Forbidden - permission denied
118154
if (response.status === 403) {
155+
// Use error_code for specific permission messages
156+
if (errorCode === "CLIENT_READ_ONLY") {
157+
throw new Error("Clients have read-only access to this resource.");
158+
}
159+
if (errorCode === "OWNER_ONLY") {
160+
throw new Error("Only workspace owners can perform this action.");
161+
}
162+
if (errorCode === "ADMIN_ONLY" || errorCode === "MANAGER_ONLY") {
163+
throw new Error(`Access denied: ${errorMessage}`);
164+
}
119165
throw new Error(`Access denied: ${errorMessage}`);
120166
}
121167

122168
// Handle 404 Not Found
123169
if (response.status === 404) {
124-
throw new Error("Resource not found or was deleted.");
170+
// Use error_code for specific resource type
171+
const resourceMessages: Record<string, string> = {
172+
TASK_NOT_FOUND: "Task not found or was deleted.",
173+
PROJECT_NOT_FOUND: "Project not found or was deleted.",
174+
WORKSPACE_NOT_FOUND: "Workspace not found or was deleted.",
175+
SPRINT_NOT_FOUND: "Sprint not found or was deleted.",
176+
USER_NOT_FOUND: "User not found.",
177+
USER_STORY_NOT_FOUND: "User story not found or was deleted.",
178+
};
179+
throw new Error(resourceMessages[errorCode] || "Resource not found or was deleted.");
125180
}
126181

127-
// Handle 409 Conflict - resource conflict (e.g., duplicate, pending operation)
182+
// Handle 409 Conflict - validation errors and business rule violations
128183
if (response.status === 409) {
184+
// VALIDATION_FAILED includes field-level errors
185+
if (errorCode === "VALIDATION_FAILED" && errorData.errors) {
186+
const fieldErrors = Object.entries(errorData.errors)
187+
.map(([field, msgs]) => `${field}: ${msgs.join(", ")}`)
188+
.join("; ");
189+
throw new Error(`Validation failed: ${fieldErrors}`);
190+
}
191+
// Other conflict errors
129192
throw new Error(`Conflict: ${errorMessage}`);
130193
}
131194

132-
// Handle 422 Unprocessable Entity - validation errors
133-
if (response.status === 422) {
134-
throw new Error(JSON.stringify({
135-
error: "validation_failed",
136-
message: errorMessage,
137-
validation_errors: errorData.errors
138-
}));
139-
}
140-
141-
// Handle 429 Too Many Requests - MCP rate limit exceeded
195+
// Handle 429 Too Many Requests - rate limit exceeded
142196
if (response.status === 429) {
197+
const retryAfter = errorData.retry_after || parseInt(response.headers.get("Retry-After") || "60");
143198
throw new Error(JSON.stringify({
144-
error: "rate_limit_exceeded",
145-
limit: response.headers.get("X-MCP-RateLimit-Limit"),
146-
remaining: response.headers.get("X-MCP-RateLimit-Remaining"),
147-
reset: response.headers.get("X-MCP-RateLimit-Reset"),
148-
upgrade_url: "https://gitscrum.com/pricing"
199+
error_code: errorCode || "RATE_LIMIT_EXCEEDED",
200+
message: errorMessage,
201+
retry_after: retryAfter,
202+
plan: errorData.plan,
203+
upgrade_url: errorData.upgrade_url || "https://gitscrum.com/pricing"
149204
}));
150205
}
151206

152207
// Handle 500+ Server Errors
153208
if (response.status >= 500) {
154-
throw new Error(`Server error: ${errorMessage}`);
209+
throw new Error(`Server error: ${errorMessage}. Please try again later.`);
155210
}
156211

157212
throw new Error(errorMessage);

0 commit comments

Comments
 (0)