UNPKG

@smartbear/mcp

Version:

MCP server for interacting SmartBear Products

330 lines (329 loc) 14 kB
/** * 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); }