feat: surface API 401 error message with login suggestion in portal generation - #297
Conversation
…eneration Previously, running portal commands while logged out showed the generic "An unexpected error occurred" message because the SDK's typed 401 error (UnauthorizedResponseError) was not recognized by the axios-only handleServiceError. Handle ApiError with status 401 in generatePortal, extract the API's message, and pair it with a suggestion to run `apimatic auth login` or pass `--auth-key`. Co-Authored-By: Claude Fable 5 <[email protected]>
0d32c05 to
adaa8ae
Compare
…rviceError Move the 401 message extraction out of generatePortal's catch and into handleServiceError, which now recognizes SDK ApiError statuses (401/404/500) alongside axios errors. This surfaces the API's 401 message + login hint for every SDK-based flow, not just portal generation, and removes the unreachable 401 branch inside the ProblemDetailsError block. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
SonarCloud flagged handleServiceError at cognitive complexity 16 (limit 15). Move the SDK ApiError status mapping into its own mapApiError function, pulling the nested conditionals out of handleServiceError. Behavior is unchanged: 401 -> unauthorized, 404 -> NotFound, other statuses -> ServerError. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…h blocks The ProblemDetailsError 400/403 handling was triplicated across generatePortal, generateSdk and generateV4Sdk, which SonarQube flagged as duplicated new code. Move it into mapProblemDetailsError (reached via handleServiceError, since ProblemDetailsError extends ApiError) and collapse all three generation catch blocks to a single handleServiceError call. Behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
4325553 to
948e8aa
Compare
| } | ||
|
|
||
| export function handleServiceError(error: unknown): ServiceError { | ||
| if (error instanceof ApiError) return mapApiError(error); |
There was a problem hiding this comment.
handleServiceError is shared by 7 services (api-, auth-, file-download-, publishing-api-, validation-, portal-service). The new if (error instanceof ApiError) branch changes behavior for every SDK-based caller, not just portal.Is this intentional
There was a problem hiding this comment.
In practice portal-service is the only caller that routes an SDK ApiError through it today, every other service (api, auth, file-download, publishing-api, validation) is axios-based and still hits the axios branch unchanged, and transformation-service uses its own handler and never calls handleServiceError. So there's no behavior change for non-portal callers right now.
In the first commit the error handling was done inside portal-service. The refactor commits later on moved these to error-service
| static notFound(customMessage: string): ServiceError { | ||
| return new ServiceError(ServiceErrorCode.NotFound, customMessage, {}); | ||
| } | ||
| static unauthorized(apiMessage: string | null): ServiceError { |
There was a problem hiding this comment.
ServiceError.UnAuthorized (singleton, generic message) vs ServiceError.unauthorized() (factory, enhanced message) differ only by casing. Both yield ServiceErrorCode.UnAuthorized. This is easy to mistype and produces two silently different behaviors. Consider a more distinct name (e.g. unauthorizedWithHint).
… mapping
Rename ServiceError.unauthorized -> unauthorizedWithHint so the enhanced-message
factory no longer collides by casing with the generic UnAuthorized singleton.
Guard mapProblemDetailsError against an absent ProblemDetails result/errors/title:
default errors to {}, only append the field message when present (no trailing
"- null"), and fall back to a "Request failed." title.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|



Problem
Running portal commands without a valid session (e.g.
apimatic portal servewhile logged out) showed the generic fallback:even though the API returns a clear reason (
{"message": "Authorization has been denied for this request."}). The SDK throws its typedUnauthorizedResponseErrorfor 401s, buthandleServiceErroronly recognizes axios errors, so the genericServerError.Changes
portal-service.ts: IngeneratePortal's catch, handleApiErrorwith status 401 — extract the API's message from the deserialized errorresultand return aServiceErrorthat pairs it with a suggested fix. All other errors continue to flow throughhandleServiceErrorunchanged (kept scoped on purpose; message handling differs per flow).service-error.ts: NewServiceError.unauthorized(apiMessage)factory (alongsidebadRequest/forbidden/notFound) that appends "runapimatic auth loginor provide a valid auth key using the--auth-keyflag" to the API's message.Result
■ Portal Generation failed.
■ Authorization has been denied for this request. Please
to log in, or provide a valid auth key using the --auth-key flag.
Testing
portal servewith an empty config dir (logged out) against the production API — enhanced message shown.auth statuswith an invalid stored key still prints "Invalid API key provided." (relies on theServiceError.UnAuthorizedsingleton identity check, which is preserved).pnpm test: 45 passing, lint clean.