diff --git a/src/infrastructure/service-error.ts b/src/infrastructure/service-error.ts index 0dca88ae..32049fd8 100644 --- a/src/infrastructure/service-error.ts +++ b/src/infrastructure/service-error.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { ApiError, ProblemDetailsError } from "@apimatic/sdk"; import { format as f } from "../prompts/format.js"; export enum ServiceErrorCode { @@ -30,6 +31,14 @@ export class ServiceError { static notFound(customMessage: string): ServiceError { return new ServiceError(ServiceErrorCode.NotFound, customMessage, {}); } + static unauthorizedWithHint(apiMessage: string | null): ServiceError { + const message = `${apiMessage ?? "You are not authorized to perform this action."} Please run ${f.cmdAlt( + "apimatic", + "auth", + "login" + )} to log in, or provide a valid auth key using the ${f.flag("auth-key")} flag.`; + return new ServiceError(ServiceErrorCode.UnAuthorized, message, {}); + } static readonly values: ServiceError[] = [ ServiceError.NotFound, @@ -54,7 +63,39 @@ export class ServiceError { } } +// A ProblemDetails body carries a `title` plus a map of field errors; surface +// the title and the first field message. +function mapProblemDetailsError(error: ProblemDetailsError): ServiceError | null { + // ProblemDetails.title and .errors are optional and result may be absent — guard + // all three so a sparse body can't crash or render a trailing "- null". + // TODO: This only picks the first error message, improve it to show all errors. + const errors = (error.result?.errors ?? {}) as Record; + const firstFieldMessage = Object.values(errors)[0]?.[0]; + const title = error.result?.title ?? "Request failed."; + const errorMessage = firstFieldMessage ? `${title}\n- ${firstFieldMessage}` : title; + if (error.statusCode === 400) return ServiceError.badRequest(errorMessage, errors); + if (error.statusCode === 403) return ServiceError.forbidden(errorMessage); + return null; +} + +// SDK controllers throw typed `ApiError`s (not axios errors). The API reports +// the reason as {"message": "..."} deserialized into `result`. +function mapApiError(error: ApiError): ServiceError { + if (error instanceof ProblemDetailsError) { + const serviceError = mapProblemDetailsError(error); + if (serviceError) return serviceError; + } + if (error.statusCode === 401) { + const apiMessage = (error.result as { message?: string } | undefined)?.message ?? null; + return ServiceError.unauthorizedWithHint(apiMessage); + } + if (error.statusCode === 404) return ServiceError.NotFound; + return ServiceError.ServerError; +} + export function handleServiceError(error: unknown): ServiceError { + if (error instanceof ApiError) return mapApiError(error); + if (axios.isAxiosError(error)) { const status = error.response?.status; if (status === 401) return ServiceError.UnAuthorized; diff --git a/src/infrastructure/services/portal-service.ts b/src/infrastructure/services/portal-service.ts index e585d77e..b4bbc1ee 100644 --- a/src/infrastructure/services/portal-service.ts +++ b/src/infrastructure/services/portal-service.ts @@ -6,7 +6,6 @@ import { ContentType, DocsPortalGenerationAsyncController, SdkSourceTreeGenerationAsyncController, - ProblemDetailsError, FileWrapper, TransformationController, Transformation, @@ -66,20 +65,8 @@ export class PortalService { ); generationId = portalInstance.result.id; } catch (error) { - if (error instanceof ProblemDetailsError) { - const errors = error.result!.errors as Record; - // TODO: This only picks the first error message, improve it to show all errors. - const message = Object.values(errors)[0]?.[0] ?? null; - const errorMessage = error.result!.title + "\n- " + message; - if (error.statusCode === 400) { - return err(ServiceError.badRequest(errorMessage, errors)); - } - if (error.statusCode === 403) { - return err(ServiceError.forbidden(errorMessage)); - } - } - const serviceError = handleServiceError(error); - return err(serviceError); + // ProblemDetails (400/403), 401 and other SDK statuses are mapped centrally. + return err(handleServiceError(error)); } finally { buildFileStream.close(); } @@ -155,20 +142,7 @@ export class PortalService { ); generationId = response.result.id; } catch (error) { - if (error instanceof ProblemDetailsError) { - // TODO: This only picks the first error message, improve it to show all errors. - const errors = error.result!.errors as Record; - const message = Object.values(errors)[0]?.[0] ?? null; - const errorMessage = error.result!.title + "\n- " + message; - if (error.statusCode === 400) { - return err(ServiceError.badRequest(errorMessage, errors)); - } - if (error.statusCode === 403) { - return err(ServiceError.forbidden(errorMessage)); - } - } - const serviceError = handleServiceError(error); - return err(serviceError); + return err(handleServiceError(error)); } finally { buildFileStream.close(); } @@ -253,20 +227,7 @@ export class PortalService { ); generationId = response.result.id; } catch (error) { - if (error instanceof ProblemDetailsError) { - // TODO: This only picks the first error message, improve it to show all errors. - const errors = error.result!.errors as Record; - const message = Object.values(errors)[0]?.[0] ?? null; - const errorMessage = error.result!.title + '\n- ' + message; - if (error.statusCode === 400) { - return err(ServiceError.badRequest(errorMessage, errors)); - } - if (error.statusCode === 403) { - return err(ServiceError.forbidden(errorMessage)); - } - } - const serviceError = handleServiceError(error); - return err(serviceError); + return err(handleServiceError(error)); } finally { buildFileStream.close(); }