From 6d7d5494dd5c6a220b8e9dd36439764ae6eadb48 Mon Sep 17 00:00:00 2001 From: Michael Anstis Date: Tue, 26 Mar 2024 12:47:00 +0000 Subject: [PATCH] AAP-19228: Refactor error handling in wisdom service and vscode extension (#1165) * AAP-19228: Refactor error handling in wisdom service and vscode extension Incorporates: - AAP-19005: Improve error handing for 400 bad request - AAP-20093: 'User not authorized to access Ansible Lightspeed' with no explanation * Update wording following peer review. --------- Co-authored-by: Michael Anstis --- .gitignore | 2 + src/features/lightspeed/api.ts | 45 +-- src/features/lightspeed/errors.ts | 213 +++++++++++++ src/features/lightspeed/handleApiError.ts | 104 +++---- test/units/lightspeed/handleApiError.test.ts | 306 ++++++++++++++++--- 5 files changed, 530 insertions(+), 140 deletions(-) create mode 100644 src/features/lightspeed/errors.ts diff --git a/.gitignore b/.gitignore index 021042a0c..2dff586f6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ coverage .nyc_output .idea *.pyc + +test/testFixtures/.vscode/ diff --git a/src/features/lightspeed/api.ts b/src/features/lightspeed/api.ts index b566d18c8..d969967ac 100644 --- a/src/features/lightspeed/api.ts +++ b/src/features/lightspeed/api.ts @@ -215,32 +215,7 @@ export class LightSpeedAPI { return response.data; } catch (error) { const err = error as AxiosError; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data: any = err?.response?.data; - if (err && "response" in err) { - if (err?.response?.status === 401) { - vscode.window.showErrorMessage( - "User not authorized to access Ansible Lightspeed." - ); - } else if ( - err?.response?.status === 403 && - (data?.code === "permission_denied__user_with_no_seat" || - data?.code === - "permission_denied__org_not_ready_because_wca_not_configured") - ) { - vscode.window.showErrorMessage( - "You must be connected to a model to send Ansible Lightspeed feedback." - ); - } else if (err?.response?.status === 400) { - console.error(`Bad Request response. Please open an Github issue.`); - } else { - console.error( - "Ansible Lightspeed encountered an error while sending feedback." - ); - } - } else { - console.error("Failed to send feedback to Ansible Lightspeed."); - } + vscode.window.showErrorMessage(retrieveError(err)); return {} as FeedbackResponseParams; } } @@ -281,23 +256,7 @@ export class LightSpeedAPI { return response.data; } catch (error) { const err = error as AxiosError; - if (err && "response" in err) { - if (err?.response?.status === 401) { - vscode.window.showErrorMessage( - "User not authorized to access Ansible Lightspeed." - ); - } else if (err?.response?.status === 400) { - console.error(`Bad Request response. Please open an Github issue.`); - } else { - console.error( - "Ansible Lightspeed encountered an error while fetching content matches." - ); - } - } else { - console.error( - "Failed to fetch content matches from Ansible Lightspeed." - ); - } + vscode.window.showErrorMessage(retrieveError(err)); return {} as ContentMatchesResponseParams; } } diff --git a/src/features/lightspeed/errors.ts b/src/features/lightspeed/errors.ts new file mode 100644 index 000000000..7d36e4d72 --- /dev/null +++ b/src/features/lightspeed/errors.ts @@ -0,0 +1,213 @@ +class ErrorHolder { + code: string; + message?: string; + + public constructor(code: string, message?: string) { + this.code = code; + this.message = message; + } +} + +class Errors { + private errors: Map> = new Map(); + + public addError(statusCode: number, error: ErrorHolder) { + if (!this.errors.has(statusCode)) { + this.errors.set(statusCode, []); + } + this.errors.get(statusCode)?.push(error); + } + + public getError(statusCode: number, code: string): ErrorHolder | undefined { + if (!this.errors.has(statusCode)) { + return undefined; + } + const errors: Array | undefined = this.errors.get(statusCode); + if (!errors) { + return undefined; + } + const e: ErrorHolder | undefined = errors.find(function (el: ErrorHolder) { + return el.code === code; + }); + if (e) { + return e; + } + return undefined; + } +} + +export const ERRORS = new Errors(); + +export const ERRORS_UNAUTHORIZED = new ErrorHolder( + "fallback__unauthorized", + "You are not authorized to access Ansible Lightspeed. Please contact your administrator." +); +export const ERRORS_TOO_MANY_REQUESTS = new ErrorHolder( + "fallback__too_many_requests", + "Too many requests to Ansible Lightspeed. Please try again later." +); +export const ERRORS_BAD_REQUEST = new ErrorHolder( + "fallback__bad_request", + "Bad Request response. Please try again." +); +export const ERRORS_UNKNOWN = new ErrorHolder( + "fallback__unknown", + "An error occurred attempting to complete your request. Please try again later." +); +export const ERRORS_CONNECTION_TIMEOUT = new ErrorHolder( + "fallback__connection_timeout", + "Ansible Lightspeed connection timeout. Please try again later." +); + +ERRORS.addError( + 204, + new ErrorHolder( + "postprocess_error", + "An error occurred post-processing the inline suggestion. Please contact your administrator." + ) +); +ERRORS.addError( + 204, + new ErrorHolder( + "model_timeout", + "Ansible Lightspeed timed out processing your request. Please try again later." + ) +); +ERRORS.addError( + 204, + new ErrorHolder( + "error__wca_bad_request", + "IBM watsonx Code Assistant returned a bad request response. Please contact your administrator." + ) +); +ERRORS.addError( + 204, + new ErrorHolder( + "error__wca_empty_response", + "IBM watsonx Code Assistant returned an empty response. Please contact your administrator." + ) +); + +ERRORS.addError( + 400, + new ErrorHolder( + "error__wca_cloud_flare_rejection", + "Cloudflare rejected the request. Please contact your administrator." + ) +); +ERRORS.addError( + 400, + new ErrorHolder( + "error__preprocess_invalid_yaml", + "An error occurred pre-processing the inline suggestion due to invalid YAML. Please contact your administrator." + ) +); +ERRORS.addError(400, new ErrorHolder("error__feedback_validation")); +ERRORS.addError( + 400, + new ErrorHolder( + "error__wca_suggestion_correlation_failed", + "IBM watsonx Code Assistant request/response correlation failed. Please contact your administrator." + ) +); + +ERRORS.addError( + 403, + new ErrorHolder( + "error__wca_invalid_model_id", + "IBM watsonx Code Assistant Model ID is invalid. Please contact your administrator." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "error__wca_key_not_found", + "Could not find an API Key for IBM watsonx Code Assistant. Please contact your administrator." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "error__wca_model_id_not_found", + "Could not find a Model Id for IBM watsonx Code Assistant. Please contact your administrator." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "permission_denied__user_trial_expired", + "Your trial to the generative AI model has expired. Refer to your IBM Cloud Account to re-enable access to the IBM watsonx Code Assistant by moving to one of the paid plans." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "permission_denied__terms_of_use_not_accepted", + "You have not accepted the Terms of Use. Please accept them before proceeding." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "permission_denied__user_not_org_administrator", + "You are not an Administrator of the Organization." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "permission_denied__user_has_no_subscription", + "Your organization does not have a subscription. Please contact your administrator." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "permission_denied__org_ready_user_has_no_seat", + "You do not have a licensed seat for Ansible Lightspeed and your organization is using the paid commercial service. Contact your Red Hat Organization's administrator for more information on how to get a licensed seat." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "permission_denied__org_not_ready_because_wca_not_configured", + "Contact your administrator to configure IBM watsonx Code Assistant model settings for your organization." + ) +); +ERRORS.addError( + 403, + new ErrorHolder( + "permission_denied__user_with_no_seat", + "You don't have access to IBM watsonx Code Assistant. Please contact your administrator." + ) +); + +ERRORS.addError( + 500, + new ErrorHolder( + "internal_server", + "An error occurred attempting to complete your request. Please try again later." + ) +); +ERRORS.addError( + 500, + new ErrorHolder( + "error__feedback_internal_server", + "An error occurred attempting to submit your feedback. Please try again later." + ) +); + +ERRORS.addError( + 503, + new ErrorHolder( + "error__attribution_exception", + "An error occurred attempting to complete your request. Please try again later." + ) +); +ERRORS.addError( + 503, + new ErrorHolder( + "service_unavailable", + "The IBM watsonx Code Assistant is unavailable. Please try again later." + ) +); diff --git a/src/features/lightspeed/handleApiError.ts b/src/features/lightspeed/handleApiError.ts index 96df74f2b..2bcdfd8dc 100644 --- a/src/features/lightspeed/handleApiError.ts +++ b/src/features/lightspeed/handleApiError.ts @@ -1,25 +1,40 @@ import { AxiosError } from "axios"; +import { + ERRORS, + ERRORS_UNAUTHORIZED, + ERRORS_TOO_MANY_REQUESTS, + ERRORS_BAD_REQUEST, + ERRORS_UNKNOWN, + ERRORS_CONNECTION_TIMEOUT, +} from "./errors"; export function retrieveError(err: AxiosError): string { if (err && "response" in err) { - if (err?.response?.status === 401) { - return "User not authorized to access Ansible Lightspeed."; - } else if (err?.response?.status === 429) { - return "Too many requests to Ansible Lightspeed. Please try again after some time."; - } else if (err?.response?.status === 400) { - const responseErrorData = >( - err?.response?.data - ); - if ( - responseErrorData && - responseErrorData.hasOwnProperty("message") && - responseErrorData.message?.includes("Cloudflare") - ) { - return `Cloudflare rejected the request. Please contact your administrator.`; - } else { - return "Bad Request response. Please try again."; - } - } else if (err?.response?.status === 403) { + const responseErrorData = >( + err?.response?.data + ); + // Lookup _known_ errors + const status: number = err?.response?.status ?? 500; + const code: string = responseErrorData.hasOwnProperty("code") + ? (responseErrorData.code as string) + : "unknown"; + const message = responseErrorData.hasOwnProperty("message") + ? (responseErrorData.message as string) + : "unknown"; + const mappedError = ERRORS.getError(status, code); + if (mappedError) { + return mappedError.message || message; + } + + // If the error is unknown fallback to defaults + if (status === 400) { + return ERRORS_BAD_REQUEST.message || message; + } + if (status === 401) { + return ERRORS_UNAUTHORIZED.message || message; + } + if (status === 403) { + // Special case where the error is not from the backend service if ( (err?.response?.headers["server"] || "").toLowerCase() === "cloudfront" ) { @@ -29,48 +44,19 @@ export function retrieveError(err: AxiosError): string { "line and column where you requested a suggestion." ); } else { - const responseErrorData = >( - err?.response?.data - ); - if ( - responseErrorData && - responseErrorData.hasOwnProperty("message") && - responseErrorData.message?.includes("WCA Model ID is invalid") - ) { - return `Model ID is invalid. Please contact your administrator.`; - } else if (responseErrorData) { - switch ( - responseErrorData.hasOwnProperty("code") && - responseErrorData.code - ) { - case "permission_denied__org_ready_user_has_no_seat": { - return "You do not have a licensed seat for Ansible Lightspeed and your organization is using the paid commercial service. Contact your Red Hat Organization's administrator for more information on how to get a licensed seat."; - } - case "permission_denied__org_not_ready_because_wca_not_configured": { - return "Contact your administrator to configure IBM watsonx Code Assistant model settings for your organization."; - } - case "permission_denied__user_trial_expired": { - return "Your trial to the generative AI model has expired. Refer to your IBM Cloud Account to re-enable access to the IBM watsonx Code Assistant by moving to one of the paid plans."; - } - case "permission_denied__user_with_no_seat": { - return "You don't have access to IBM watsonx Code Assistant. Contact your administrator."; - } - default: { - return "User not authorized to access Ansible Lightspeed."; - } - } - } else { - return "User not authorized to access Ansible Lightspeed."; - } + return ERRORS_UNAUTHORIZED.message || message; } - } else if (err?.response?.status.toString().startsWith("5")) { - return "Ansible Lightspeed encountered an error. Try again after some time."; - } else { - return `Failed to fetch inline suggestion from Ansible Lightspeed with status code: ${err?.response?.status}. Try again after some time.`; } - } else if (err.code === AxiosError.ECONNABORTED) { - return "Ansible Lightspeed connection timeout. Try again after some time."; - } else { - return "Failed to fetch inline suggestion from Ansible Lightspeed. Try again after some time."; + if (status === 429) { + return ERRORS_TOO_MANY_REQUESTS.message || message; + } + if (status === 500) { + return ERRORS_UNKNOWN.message || message; + } + } + + if (err.code === AxiosError.ECONNABORTED) { + return ERRORS_CONNECTION_TIMEOUT.message as string; } + return ERRORS_UNKNOWN.message as string; } diff --git a/test/units/lightspeed/handleApiError.test.ts b/test/units/lightspeed/handleApiError.test.ts index ab8c6c9ef..5cdb8233f 100644 --- a/test/units/lightspeed/handleApiError.test.ts +++ b/test/units/lightspeed/handleApiError.test.ts @@ -37,47 +37,139 @@ function createError( } describe("testing the error handling", () => { + // ================================= + // HTTP 200 + // --------------------------------- it("err generic", () => { const msg = retrieveError(createError(200)); assert.equal( msg, - "Failed to fetch inline suggestion from Ansible Lightspeed with status code: 200. Try again after some time." + "An error occurred attempting to complete your request. Please try again later." ); }); - it("err Unauthorized", () => { - const msg = retrieveError(createError(401)); - assert.equal(msg, "User not authorized to access Ansible Lightspeed."); + // ================================= + + // ================================= + // HTTP 204 + // --------------------------------- + it("err Postprocessing error", () => { + const msg = retrieveError( + createError(204, { + code: "postprocess_error", + }) + ); + assert.equal( + msg, + "An error occurred post-processing the inline suggestion. Please contact your administrator." + ); }); - it("err Too Many Requests", () => { - const msg = retrieveError(createError(429)); + + it("err Model timeout", () => { + const msg = retrieveError( + createError(204, { + code: "model_timeout", + }) + ); + assert.equal( + msg, + "Ansible Lightspeed timed out processing your request. Please try again later." + ); + }); + + it("err WCA Bad Request", () => { + const msg = retrieveError( + createError(204, { + code: "error__wca_bad_request", + }) + ); assert.equal( msg, - "Too many requests to Ansible Lightspeed. Please try again after some time." + "IBM watsonx Code Assistant returned a bad request response. Please contact your administrator." ); }); + + it("err WCA Empty Response", () => { + const msg = retrieveError( + createError(204, { + code: "error__wca_empty_response", + }) + ); + assert.equal( + msg, + "IBM watsonx Code Assistant returned an empty response. Please contact your administrator." + ); + }); + // ================================= + + // ================================= + // HTTP 400 + // --------------------------------- it("err Bad Request from Cloudflare", () => { const msg = retrieveError( - createError(400, { message: "Some string from Cloudflare." }) + createError(400, { code: "error__wca_cloud_flare_rejection" }) ); assert.equal( msg, "Cloudflare rejected the request. Please contact your administrator." ); }); + it("err Bad Request", () => { const msg = retrieveError(createError(400)); assert.equal(msg, "Bad Request response. Please try again."); }); - it("err Forbidden - WCA Model ID is invalid", () => { + + it("err Postprocessing error", () => { const msg = retrieveError( - createError(403, { message: "WCA Model ID is invalid" }) + createError(400, { + code: "error__preprocess_invalid_yaml", + }) ); assert.equal( msg, - `Model ID is invalid. Please contact your administrator.` + "An error occurred pre-processing the inline suggestion due to invalid YAML. Please contact your administrator." ); }); - it("err Forbidden - No seat", () => { + + it("err Feedback validation error", () => { + const msg = retrieveError( + createError(400, { + code: "error__feedback_validation", + message: "A field was invalid.", + }) + ); + assert.equal(msg, "A field was invalid."); + }); + + it("err WCA Suggestion Correlation failure", () => { + const msg = retrieveError( + createError(400, { + code: "error__wca_suggestion_correlation_failed", + }) + ); + assert.equal( + msg, + "IBM watsonx Code Assistant request/response correlation failed. Please contact your administrator." + ); + }); + // ================================= + + // ================================= + // HTTP 401 + // --------------------------------- + it("err Unauthorized", () => { + const msg = retrieveError(createError(401)); + assert.equal( + msg, + "You are not authorized to access Ansible Lightspeed. Please contact your administrator." + ); + }); + // ================================= + + // ================================= + // HTTP 403 + // --------------------------------- + it("err Forbidden - Org ready, No seat", () => { const msg = retrieveError( createError(403, { code: "permission_denied__org_ready_user_has_no_seat", @@ -85,9 +177,22 @@ describe("testing the error handling", () => { ); assert.equal( msg, - `You do not have a licensed seat for Ansible Lightspeed and your organization is using the paid commercial service. Contact your Red Hat Organization's administrator for more information on how to get a licensed seat.` + "You do not have a licensed seat for Ansible Lightspeed and your organization is using the paid commercial service. Contact your Red Hat Organization's administrator for more information on how to get a licensed seat." + ); + }); + + it("err Forbidden - No Seat", () => { + const msg = retrieveError( + createError(403, { + code: "permission_denied__user_with_no_seat", + }) + ); + assert.equal( + msg, + "You don't have access to IBM watsonx Code Assistant. Please contact your administrator." ); }); + it("err Forbidden - Trial expired", () => { const msg = retrieveError( createError(403, { @@ -96,9 +201,10 @@ describe("testing the error handling", () => { ); assert.equal( msg, - `Your trial to the generative AI model has expired. Refer to your IBM Cloud Account to re-enable access to the IBM watsonx Code Assistant by moving to one of the paid plans.` + "Your trial to the generative AI model has expired. Refer to your IBM Cloud Account to re-enable access to the IBM watsonx Code Assistant by moving to one of the paid plans." ); }); + it("err Forbidden - WCA not ready", () => { const msg = retrieveError( createError(403, { @@ -107,46 +213,175 @@ describe("testing the error handling", () => { ); assert.equal( msg, - `Contact your administrator to configure IBM watsonx Code Assistant model settings for your organization.` + "Contact your administrator to configure IBM watsonx Code Assistant model settings for your organization." ); }); - it("err Forbidden - No Seat", () => { + + it("err Forbidden", () => { + const msg = retrieveError(createError(403)); + assert.equal( + msg, + "You are not authorized to access Ansible Lightspeed. Please contact your administrator." + ); + }); + + it("err Bad Request from CloudFront", () => { + const msg = retrieveError( + createError( + 403, + { data: "Some string from CloudFront." }, + { server: "CloudFront" } + ) + ); + assert.match( + msg, + /Something in your editor content has caused your inline suggestion request to be blocked.*/ + ); + }); + + it("err WCA API Key missing", () => { const msg = retrieveError( createError(403, { - code: "permission_denied__user_with_no_seat", + code: "error__wca_key_not_found", }) ); assert.equal( msg, - "You don't have access to IBM watsonx Code Assistant. Contact your administrator." + "Could not find an API Key for IBM watsonx Code Assistant. Please contact your administrator." ); }); - it("err Forbidden", () => { - const msg = retrieveError(createError(403)); - assert.equal(msg, `User not authorized to access Ansible Lightspeed.`); + it("err WCA Model Id missing", () => { + const msg = retrieveError( + createError(403, { + code: "error__wca_model_id_not_found", + }) + ); + assert.equal( + msg, + "Could not find a Model Id for IBM watsonx Code Assistant. Please contact your administrator." + ); }); - it("err Internal Server Error", () => { + + it("err WCA Model Id is invalid", () => { + const msg = retrieveError( + createError(403, { + code: "error__wca_invalid_model_id", + }) + ); + assert.equal( + msg, + "IBM watsonx Code Assistant Model ID is invalid. Please contact your administrator." + ); + }); + + it("err Terms of Use not accepted", () => { + const msg = retrieveError( + createError(403, { + code: "permission_denied__terms_of_use_not_accepted", + }) + ); + assert.equal( + msg, + "You have not accepted the Terms of Use. Please accept them before proceeding." + ); + }); + + it("err User has no subscription", () => { + const msg = retrieveError( + createError(403, { + code: "permission_denied__user_has_no_subscription", + }) + ); + assert.equal( + msg, + "Your organization does not have a subscription. Please contact your administrator." + ); + }); + // ================================= + + // ================================= + // HTTP 429 + // --------------------------------- + it("err Too Many Requests", () => { + const msg = retrieveError(createError(429)); + assert.equal( + msg, + "Too many requests to Ansible Lightspeed. Please try again later." + ); + }); + // ================================= + + // ================================= + // HTTP 500 + // --------------------------------- + it("err Internal Server Error - Generic", () => { const msg = retrieveError(createError(500)); assert.equal( msg, - `Ansible Lightspeed encountered an error. Try again after some time.` + "An error occurred attempting to complete your request. Please try again later." ); }); - it("err Unexpected Err code", () => { - const msg = retrieveError(createError(999)); + + it("err Internal Server Error - Codified", () => { + const msg = retrieveError(createError(500, { code: "internal_server" })); + assert.equal( + msg, + "An error occurred attempting to complete your request. Please try again later." + ); + }); + + it("err Error submitting feedback", () => { + const msg = retrieveError( + createError(500, { + code: "error__feedback_internal_server", + }) + ); + assert.equal( + msg, + "An error occurred attempting to submit your feedback. Please try again later." + ); + }); + // ================================= + + // ================================= + // HTTP 503 + // --------------------------------- + it("err Attribution error", () => { + const msg = retrieveError( + createError(503, { + code: "error__attribution_exception", + }) + ); assert.equal( msg, - "Failed to fetch inline suggestion from Ansible Lightspeed with status code: 999. Try again after some time." + "An error occurred attempting to complete your request. Please try again later." ); }); + + it("err Service unavailable", () => { + const msg = retrieveError( + createError(503, { + code: "service_unavailable", + }) + ); + assert.equal( + msg, + "The IBM watsonx Code Assistant is unavailable. Please try again later." + ); + }); + // ================================= + + // ================================= + // Miscellaneous + // --------------------------------- it("err Timeout", () => { const err = createError(0); err.code = AxiosError.ECONNABORTED; const msg = retrieveError(err); assert.equal( msg, - "Ansible Lightspeed connection timeout. Try again after some time." + "Ansible Lightspeed connection timeout. Please try again later." ); }); @@ -154,21 +389,16 @@ describe("testing the error handling", () => { const msg = retrieveError(createError(0)); assert.equal( msg, - "Failed to fetch inline suggestion from Ansible Lightspeed. Try again after some time." + "An error occurred attempting to complete your request. Please try again later." ); }); - it("err Bad Request from CloudFront", () => { - const msg = retrieveError( - createError( - 403, - { data: "Some string from CloudFront." }, - { server: "CloudFront" } - ) - ); - assert.match( + it("err Unexpected Err code", () => { + const msg = retrieveError(createError(999)); + assert.equal( msg, - /Something in your editor content has caused your inline suggestion request to be blocked.*/ + "An error occurred attempting to complete your request. Please try again later." ); }); + // ================================= });