feat(error-handling): Implement comprehensive gateway error handling with user-friendly messages
- Add comprehensive gateway error handler with friendly user messages - Handle subscription status errors (inactive BYOK, subscription required) - Handle authentication errors (invalid API keys, missing tokens) - Handle rate limiting with retry suggestions - Handle model availability and validation errors - Handle network connectivity issues - Provide actionable solutions for each error type - Prevent duplicate error messages by returning early after showing friendly error - Fix telemetry tests to use correct environment variable names (TASKMASTER_API_KEY) - Fix config manager getUserId function to properly save default userId to file - All tests now passing (34 test suites, 360 tests)
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
||||
} from "../../src/ai-providers/index.js";
|
||||
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { handleGatewayError } from "./utils/gatewayErrorHandler.js";
|
||||
|
||||
// Create provider instances
|
||||
const PROVIDERS = {
|
||||
@@ -346,31 +347,51 @@ async function _callGatewayAI(
|
||||
headers["X-User-Email"] = userEmail;
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Gateway AI call failed: ${response.status} ${errorText}`);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Gateway AI call failed: ${response.status} ${errorText}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Gateway AI call failed");
|
||||
}
|
||||
|
||||
// Return the AI response in the expected format
|
||||
return {
|
||||
text: result.data.text,
|
||||
object: result.data.object,
|
||||
usage: result.data.usage,
|
||||
// Include any account info returned from gateway
|
||||
accountInfo: result.accountInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
// Use the enhanced error handler for user-friendly messages
|
||||
handleGatewayError(error, commandName);
|
||||
|
||||
// Throw a much cleaner error message to prevent ugly double logging
|
||||
const match = error.message.match(/Gateway AI call failed: (\d+)/);
|
||||
if (match) {
|
||||
const statusCode = match[1];
|
||||
throw new Error(
|
||||
`TaskMaster gateway error (${statusCode}). See details above.`
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
"TaskMaster gateway communication failed. See details above."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Gateway AI call failed");
|
||||
}
|
||||
|
||||
// Return the AI response in the expected format
|
||||
return {
|
||||
text: result.data.text,
|
||||
object: result.data.object,
|
||||
usage: result.data.usage,
|
||||
// Include any account info returned from gateway
|
||||
accountInfo: result.accountInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -721,12 +721,56 @@ function getUserId(explicitRoot = null) {
|
||||
config.account = { ...defaultConfig.account };
|
||||
}
|
||||
|
||||
// Check if the userId exists in the actual file (not merged config)
|
||||
let needsToSaveUserId = false;
|
||||
|
||||
// Load the raw config to check if userId is actually in the file
|
||||
try {
|
||||
let rootPath = explicitRoot;
|
||||
if (explicitRoot === null || explicitRoot === undefined) {
|
||||
const foundRoot = findProjectRoot();
|
||||
if (!foundRoot) {
|
||||
// If no project root, can't check file, assume userId needs to be saved
|
||||
needsToSaveUserId = true;
|
||||
} else {
|
||||
rootPath = foundRoot;
|
||||
}
|
||||
}
|
||||
|
||||
if (rootPath && !needsToSaveUserId) {
|
||||
const configPath = path.join(rootPath, CONFIG_FILE_NAME);
|
||||
if (fs.existsSync(configPath)) {
|
||||
const rawConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
// Check if userId is missing from the actual file
|
||||
needsToSaveUserId = !rawConfig.account?.userId;
|
||||
} else {
|
||||
// Config file doesn't exist, need to save
|
||||
needsToSaveUserId = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's any error reading the file, assume we need to save
|
||||
needsToSaveUserId = true;
|
||||
}
|
||||
|
||||
// If userId exists and is not the placeholder, return it
|
||||
if (config.account.userId && config.account.userId !== "1234567890") {
|
||||
return config.account.userId;
|
||||
}
|
||||
|
||||
// If userId is null or the placeholder, return the placeholder
|
||||
// If userId is missing from the actual file, set the placeholder and save it
|
||||
if (needsToSaveUserId) {
|
||||
config.account.userId = "1234567890";
|
||||
const success = writeConfig(config, explicitRoot);
|
||||
if (!success) {
|
||||
console.warn("Warning: Failed to save default userId to config file");
|
||||
}
|
||||
// Force reload the cached config to reflect the change
|
||||
loadedConfig = null;
|
||||
loadedConfigRoot = null;
|
||||
}
|
||||
|
||||
// Return the placeholder
|
||||
// This signals to other code that auth/init needs to be attempted
|
||||
return "1234567890";
|
||||
}
|
||||
|
||||
186
scripts/modules/utils/gatewayErrorHandler.js
Normal file
186
scripts/modules/utils/gatewayErrorHandler.js
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Enhanced error handler for gateway responses
|
||||
* @param {Error} error - The error from the gateway call
|
||||
* @param {string} commandName - The command being executed
|
||||
*/
|
||||
function handleGatewayError(error, commandName) {
|
||||
try {
|
||||
// Extract status code and response from error message
|
||||
const match = error.message.match(/Gateway AI call failed: (\d+) (.+)/);
|
||||
if (!match) {
|
||||
throw new Error(`Unexpected error format: ${error.message}`);
|
||||
}
|
||||
|
||||
const [, statusCode, responseText] = match;
|
||||
const status = parseInt(statusCode);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(responseText);
|
||||
} catch {
|
||||
// Handle non-JSON error responses
|
||||
console.error(`[ERROR] Gateway error (${status}): ${responseText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
handleValidationError(response, commandName);
|
||||
break;
|
||||
case 401:
|
||||
handleAuthError(response, commandName);
|
||||
break;
|
||||
case 402:
|
||||
handleCreditError(response, commandName);
|
||||
break;
|
||||
case 403:
|
||||
handleAccessDeniedError(response, commandName);
|
||||
break;
|
||||
case 429:
|
||||
handleRateLimitError(response, commandName);
|
||||
break;
|
||||
case 500:
|
||||
handleServerError(response, commandName);
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
`[ERROR] Unexpected gateway error (${status}):`,
|
||||
response
|
||||
);
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error(`[ERROR] Failed to parse gateway error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleValidationError(response, commandName) {
|
||||
if (response.error?.includes("Unsupported model")) {
|
||||
console.error("🚫 The selected AI model is not supported by the gateway.");
|
||||
console.error(
|
||||
"💡 Try running `task-master models` to see available models."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.error?.includes("schema is required")) {
|
||||
console.error("🚫 This command requires a schema for structured output.");
|
||||
console.error("💡 This is likely a bug - please report it.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`🚫 Invalid request: ${response.error}`);
|
||||
if (response.details?.length > 0) {
|
||||
response.details.forEach((detail) => {
|
||||
console.error(` • ${detail.message || detail}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleAuthError(response, commandName) {
|
||||
console.error("🔐 Authentication failed with TaskMaster gateway.");
|
||||
|
||||
if (response.message?.includes("Invalid token")) {
|
||||
console.error("💡 Your auth token may have expired. Try running:");
|
||||
console.error(" task-master init");
|
||||
} else if (response.message?.includes("Missing X-TaskMaster-Service-ID")) {
|
||||
console.error(
|
||||
"💡 Service authentication issue. This is likely a bug - please report it."
|
||||
);
|
||||
} else {
|
||||
console.error("💡 Please check your authentication settings.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreditError(response, commandName) {
|
||||
console.error("💳 Insufficient credits for this operation.");
|
||||
console.error(`💡 ${response.message || "Your account needs more credits."}`);
|
||||
console.error(" • Visit your dashboard to add credits");
|
||||
console.error(" • Or upgrade to a plan with more credits");
|
||||
console.error(
|
||||
" • You can also switch to BYOK mode to use your own API keys"
|
||||
);
|
||||
}
|
||||
|
||||
function handleAccessDeniedError(response, commandName) {
|
||||
const { details, hint } = response;
|
||||
|
||||
if (
|
||||
details?.planType === "byok" &&
|
||||
details?.subscriptionStatus === "inactive"
|
||||
) {
|
||||
console.error(
|
||||
"🔒 BYOK users need active subscriptions for hosted AI services."
|
||||
);
|
||||
console.error("💡 You have two options:");
|
||||
console.error(" 1. Upgrade to a paid plan for hosted AI services");
|
||||
console.error(" 2. Switch to BYOK mode and use your own API keys");
|
||||
console.error("");
|
||||
console.error(" To use your own API keys:");
|
||||
console.error(
|
||||
" • Set your API keys in .env file (e.g., ANTHROPIC_API_KEY=...)"
|
||||
);
|
||||
console.error(" • The system will automatically use direct API calls");
|
||||
return;
|
||||
}
|
||||
|
||||
if (details?.subscriptionStatus === "past_due") {
|
||||
console.error("💳 Your subscription payment is overdue.");
|
||||
console.error(
|
||||
"💡 Please update your payment method to continue using AI services."
|
||||
);
|
||||
console.error(
|
||||
" Visit your account dashboard to update billing information."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (details?.planType === "free" && commandName === "research") {
|
||||
console.error("🔬 Research features require a paid subscription.");
|
||||
console.error("💡 Upgrade your plan to access research-powered commands.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`🔒 Access denied: ${response.message}`);
|
||||
if (hint) {
|
||||
console.error(`💡 ${hint}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRateLimitError(response, commandName) {
|
||||
const retryAfter = response.retryAfter || 60;
|
||||
console.error("⏱️ Rate limit exceeded - too many requests.");
|
||||
console.error(`💡 Please wait ${retryAfter} seconds before trying again.`);
|
||||
console.error(" Consider upgrading your plan for higher rate limits.");
|
||||
}
|
||||
|
||||
function handleServerError(response, commandName) {
|
||||
const retryAfter = response.retryAfter || 10;
|
||||
|
||||
if (response.error?.includes("Service temporarily unavailable")) {
|
||||
console.error("🚧 TaskMaster gateway is temporarily unavailable.");
|
||||
console.error(
|
||||
`💡 The service should recover automatically. Try again in ${retryAfter} seconds.`
|
||||
);
|
||||
console.error(
|
||||
" You can also switch to BYOK mode to use direct API calls."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.message?.includes("No user message found")) {
|
||||
console.error("🚫 Invalid request format - missing user message.");
|
||||
console.error("💡 This is likely a bug - please report it.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("⚠️ Gateway server error occurred.");
|
||||
console.error(
|
||||
`💡 Try again in ${retryAfter} seconds. If the problem persists:`
|
||||
);
|
||||
console.error(" • Check TaskMaster status page");
|
||||
console.error(" • Switch to BYOK mode as a workaround");
|
||||
console.error(" • Contact support if the issue continues");
|
||||
}
|
||||
|
||||
// Export the main handler function
|
||||
export { handleGatewayError };
|
||||
Reference in New Issue
Block a user