Skip to content

Commit

Permalink
AAP-19228: Refactor error handling in wisdom service and vscode exten…
Browse files Browse the repository at this point in the history
…sion (#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 <[email protected]>
  • Loading branch information
manstis and manstis authored Mar 26, 2024
1 parent 243ebeb commit 6d7d549
Show file tree
Hide file tree
Showing 5 changed files with 530 additions and 140 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ coverage
.nyc_output
.idea
*.pyc

test/testFixtures/.vscode/
45 changes: 2 additions & 43 deletions src/features/lightspeed/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
}
Expand Down
213 changes: 213 additions & 0 deletions src/features/lightspeed/errors.ts
Original file line number Diff line number Diff line change
@@ -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<number, Array<ErrorHolder>> = 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<ErrorHolder> | 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."
)
);
104 changes: 45 additions & 59 deletions src/features/lightspeed/handleApiError.ts
Original file line number Diff line number Diff line change
@@ -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 = <AxiosError<{ message?: string }>>(
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 = <AxiosError<{ message?: string }>>(
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"
) {
Expand All @@ -29,48 +44,19 @@ export function retrieveError(err: AxiosError): string {
"line and column where you requested a suggestion."
);
} else {
const responseErrorData = <AxiosError<{ message?: string }>>(
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;
}
Loading

0 comments on commit 6d7d549

Please sign in to comment.