@smartbear/mcp
Version:
MCP server for interacting SmartBear Products
330 lines (329 loc) • 14 kB
JavaScript
/**
* QMetry API Error Handling Utilities
*
* This module provides centralized error handling for QMetry API operations,
* including user-friendly error messages and troubleshooting guidance.
*
* Handles common error scenarios:
* - SSL/TLS Certificate errors (corporate proxies like Zscaler)
* - Authentication/Authorization failures
* - CORS (Cross-Origin Resource Sharing) issues
* - Project access errors
* - Network connectivity problems
* - Generic API errors with helpful troubleshooting
*/
/**
* Error message templates for common QMetry API issues
*/
const ERROR_TEMPLATES = {
AUTHENTICATION_FAILED: (baseUrl, errorText) => `QMetry API Authentication Failed: Invalid or expired API key.\n\n` +
`To resolve this issue:\n` +
`1. Log into your QMetry Test Management instance: ${baseUrl}\n` +
`2. Search Open API → Go To Open API Page\n` +
`3. Generate a new API key OR copy an existing valid key\n` +
`4. Copy the API key to your clipboard\n` +
`5. Restart VS Code or reload the MCP server\n` +
`6. When prompted, paste the new API key for 'QMetry Open API Key'\n\n` +
`Note: API keys may expire or be revoked. Always use the latest key from your QMetry instance.\n` +
`Original error: ${errorText}`,
AUTHORIZATION_ERROR: (errorText) => `QMetry Authorization Error: Insufficient permissions.\n\n` +
`Your API key is valid but lacks the necessary permissions for this operation.\n\n` +
`Possible causes:\n` +
`1. Your user role doesn't have access to this resource\n` +
`2. The project permissions are restricted\n` +
`3. The specific operation requires higher privileges\n\n` +
`To resolve:\n` +
`1. Contact your QMetry administrator to review your permissions\n` +
`2. Verify you have the correct project access\n` +
`3. Check if your user role includes the required privileges\n\n` +
`Original error: ${errorText}`,
PROJECT_ACCESS_ERROR: (project, errorText) => `QMetry Project Access Error: Cannot access project '${project}'.\n\n` +
`Possible causes:\n` +
`1. API key lacks permissions for this project\n` +
`2. Project key '${project}' doesn't exist or is archived\n` +
`3. Your user account doesn't have access to this project\n\n` +
`To resolve:\n` +
`1. Verify the project key is correct\n` +
`2. Check your QMetry permissions for this project\n` +
`3. Contact your QMetry administrator if needed\n\n` +
`Original error: ${errorText}`,
GENERIC_API_ERROR: (status, baseUrl, errorText) => `QMetry API request failed (${status}): ${errorText}\n\n` +
`Troubleshooting tips:\n` +
`Verify your QMetry instance URL: ${baseUrl}\n` +
`Check if your API key is valid and not expired\n` +
`Ensure you have the necessary permissions\n` +
`Try accessing QMetry directly in your browser to confirm connectivity`,
CORS_ERROR: (baseUrl, errorText) => `QMetry API CORS Error: Cross-Origin Request Blocked.\n\n` +
`CORS (Cross-Origin Resource Sharing) issue detected:\n` +
`Your browser is blocking the request due to CORS policy\n` +
`The QMetry server may not be configured to allow requests from this origin\n\n` +
`Possible solutions:\n` +
`1. Contact your QMetry administrator to configure CORS headers\n` +
`2. Ensure the QMetry instance URL is correct: ${baseUrl}\n` +
`3. If using a proxy, verify proxy CORS configuration\n` +
`4. Try accessing QMetry from the same domain/protocol\n` +
`5. Check if browser extensions are blocking the request\n\n` +
`Technical details: ${errorText}`,
SSL_CERTIFICATE_ERROR: (baseUrl, errorText) => `QMetry API SSL Certificate Error: Unable to verify certificate.\n\n` +
`SSL/TLS Certificate verification failed - Common in corporate networks:\n` +
`• Corporate proxy/firewall (Zscaler, Forcepoint, etc.) is intercepting HTTPS traffic\n` +
`• Self-signed or corporate certificates are being used\n` +
`• Certificate chain validation is failing\n\n` +
`Solution for corporate network environments:\n` +
`Contact your IT administrator to:\n` +
`• Add QMetry domain to certificate bypass list\n` +
`• Configure corporate certificates properly\n` +
`• Whitelist the QMetry API endpoints\n` +
`Target URL: ${baseUrl}\n` +
`Technical details: ${errorText}`,
INVALID_URL_ERROR: (baseUrl, path, errorText) => `QMetry API Invalid URL Error: The API endpoint appears to be incorrect.\n\n` +
`Request details:\n` +
`• Base URL: ${baseUrl}\n` +
`• API Path: ${path}\n` +
`• Full URL: ${baseUrl}${path}\n\n` +
`Common URL issues:\n` +
`1. Wrong API endpoint path (check QMetry API documentation)\n` +
`2. Typo in the URL path (missing '/', wrong spelling)\n` +
`3. API version mismatch (v1, v2, etc.)\n` +
`4. Incorrect base URL (should end with QMetry instance domain)\n` +
`5. Body stream errors (often caused by malformed URLs or wrong HTTP method)\n\n` +
`Troubleshooting steps:\n` +
`1. Verify the API endpoint in QMetry documentation\n` +
`2. Check the QMetry instance URL is correct\n` +
`3. Test the endpoint manually using a REST client\n` +
`4. Ensure the API path matches the expected format\n` +
`5. If you see 'Body is unusable' errors, check for URL typos or wrong endpoints\n\n` +
`Expected URL format: https://your-qmetry-instance.com/rest/...\n` +
`Server response: ${errorText}`,
};
/**
* Checks if the error is related to authentication (invalid/expired API key)
*/
function isAuthenticationError(context) {
const { status, errorData } = context;
return (status === 401 ||
errorData?.code === "CO.INVALID_API_KEY" ||
errorData?.message?.toLowerCase().includes("invalid api key"));
}
/**
* Checks if the error is related to authorization (insufficient permissions)
*/
function isAuthorizationError(context) {
const { status } = context;
return status === 403;
}
/**
* Checks if the error is related to project access
*/
function isProjectAccessError(context) {
const { status, path } = context;
return status === 404 && (path?.includes("project") ?? false);
}
/**
* Checks if the error is related to CORS (Cross-Origin Resource Sharing)
*/
function isCorsError(context) {
const { status, errorText, isCorsError } = context;
// Explicit CORS flag from fetch error
if (isCorsError) {
return true;
}
// Common CORS indicators
const corsIndicators = [
"cors",
"cross-origin",
"cross origin",
"preflight",
"access-control-allow-origin",
"network error when attempting to fetch resource",
"failed to fetch",
];
const lowercaseErrorText = errorText.toLowerCase();
// Status 0 often indicates CORS issues
if (status === 0) {
return true;
}
// Check error text for CORS-related keywords
return corsIndicators.some((indicator) => lowercaseErrorText.includes(indicator));
}
/**
* Checks if the error is related to SSL/TLS certificate issues
*/
function isSslCertificateError(context) {
const { errorText } = context;
// Common SSL certificate error indicators
const sslErrorIndicators = [
"unable to get local issuer certificate",
"unable_to_get_issuer_cert_locally",
"self signed certificate",
"certificate verify failed",
"ssl certificate problem",
"certificate has expired",
"certificate authority is invalid",
"untrusted certificate",
"cert authority invalid",
"tls certificate verification failed",
"certificate validation error",
];
const lowercaseErrorText = errorText.toLowerCase();
// Check for specific error codes that indicate SSL issues
if (lowercaseErrorText.includes("unable_to_get_issuer_cert_locally") ||
lowercaseErrorText.includes("cert_untrusted") ||
lowercaseErrorText.includes("cert_authority_invalid") ||
lowercaseErrorText.includes("certificate verify failed")) {
return true;
}
// Check error text for SSL-related keywords
return sslErrorIndicators.some((indicator) => lowercaseErrorText.includes(indicator));
}
/**
* Checks if the error is related to invalid/wrong API URL
*/
function isInvalidUrlError(context) {
const { status, errorText, errorData } = context;
const lowercaseErrorText = errorText.toLowerCase();
// HTTP 404 is the most common indicator of wrong URL/endpoint
if (status === 404) {
return true;
}
// Check for specific error messages that indicate wrong URL
const urlErrorIndicators = [
"not found",
"resource not found",
"endpoint not found",
"path not found",
"url not found",
"route not found",
"invalid endpoint",
"unknown endpoint",
"method not allowed",
"no handler found",
"404",
// Body stream errors that often indicate wrong URL/malformed requests
"body is unusable",
"body has already been read",
"request body already read",
"body stream already consumed",
"invalid request body",
"malformed request",
];
// Check error text for URL-related keywords
if (urlErrorIndicators.some((indicator) => lowercaseErrorText.includes(indicator))) {
return true;
}
// Check if error data indicates invalid endpoint
if (errorData?.error?.toLowerCase().includes("not found") ||
errorData?.message?.toLowerCase().includes("not found")) {
return true;
}
return false;
}
/**
* Creates an appropriate error message based on the error context
*/
export function createQMetryError(context) {
const { status, errorText, baseUrl, project } = context;
// SSL certificate errors should be checked first as they're network-level issues
if (isSslCertificateError(context)) {
return new Error(ERROR_TEMPLATES.SSL_CERTIFICATE_ERROR(baseUrl, errorText));
}
if (isCorsError(context)) {
return new Error(ERROR_TEMPLATES.CORS_ERROR(baseUrl, errorText));
}
if (isAuthenticationError(context)) {
return new Error(ERROR_TEMPLATES.AUTHENTICATION_FAILED(baseUrl, errorText));
}
if (isAuthorizationError(context)) {
return new Error(ERROR_TEMPLATES.AUTHORIZATION_ERROR(errorText));
}
if (isProjectAccessError(context) && project) {
return new Error(ERROR_TEMPLATES.PROJECT_ACCESS_ERROR(project, errorText));
}
// Check for invalid URL/endpoint errors (404, wrong paths, etc.)
if (isInvalidUrlError(context)) {
const path = context.path || "";
return new Error(ERROR_TEMPLATES.INVALID_URL_ERROR(baseUrl, path, errorText));
}
return new Error(ERROR_TEMPLATES.GENERIC_API_ERROR(status, baseUrl, errorText));
}
/**
* Handles QMetry API errors in a standardized way
*
* @param response - The failed HTTP response
* @param baseUrl - The QMetry base URL
* @param project - Optional project context
* @param path - Optional API path context
*/
export async function handleQMetryApiError(response, baseUrl, project, path) {
let errorData;
let errorText;
try {
errorData = await response.json();
errorText = JSON.stringify(errorData);
}
catch {
errorText = (await response.text()) || `HTTP ${response.status}`;
}
const context = {
status: response.status,
errorData,
errorText,
baseUrl,
project,
path,
};
throw createQMetryError(context);
}
/**
* Handles fetch errors that occur before receiving a response (e.g., CORS, network issues)
*
* @param error - The fetch error
* @param baseUrl - The QMetry base URL
* @param project - Optional project context
* @param path - Optional API path context
*/
export function handleQMetryFetchError(error, baseUrl, project, path) {
// Extract comprehensive error information
let errorText = error.message || error.toString();
// Check if error has a cause property with more details (common for SSL errors)
if (error.cause && typeof error.cause === "object") {
const cause = error.cause;
if (cause.message) {
errorText += ` | Cause: ${cause.message}`;
}
if (cause.code) {
errorText += ` | Code: ${cause.code}`;
}
// Add the full cause details for better debugging
if (cause.toString && typeof cause.toString === "function") {
errorText += ` | Details: ${cause.toString()}`;
}
}
// Check for specific SSL error patterns in the full error
const fullErrorString = JSON.stringify(error, Object.getOwnPropertyNames(error));
if (fullErrorString.includes("unable to get local issuer certificate") ||
fullErrorString.includes("UNABLE_TO_GET_ISSUER_CERT_LOCALLY")) {
errorText +=
" | SSL Certificate Error: Corporate proxy/firewall intercepting HTTPS";
}
// Check if this is an SSL certificate error first
const tempContext = {
status: 0,
errorText,
baseUrl,
project,
path,
};
const isSSLError = isSslCertificateError(tempContext);
const isURLError = isInvalidUrlError(tempContext);
const context = {
status: 0, // Status 0 typically indicates network/CORS/SSL issues
errorText,
baseUrl,
project,
path,
// Only assume CORS if it's not an SSL or URL error
isCorsError: !isSSLError && !isURLError,
};
throw createQMetryError(context);
}