Skip to content

feat: surface API 401 error message with login suggestion in portal generation - #297

Merged
MuhammadRafay1 merged 5 commits into
devfrom
error-message-enhancement
Jul 27, 2026
Merged

feat: surface API 401 error message with login suggestion in portal generation#297
MuhammadRafay1 merged 5 commits into
devfrom
error-message-enhancement

Conversation

@MuhammadRafay1

@MuhammadRafay1 MuhammadRafay1 commented Jul 14, 2026

Copy link
Copy Markdown

Problem

Running portal commands without a valid session (e.g. apimatic portal serve while logged out) showed the generic fallback:

An unexpected error occurred, please try again later. If the problem persists, please reach out to our team at [email protected]

even though the API returns a clear reason ({"message": "Authorization has been denied for this request."}). The SDK throws its typed UnauthorizedResponseError for 401s, but handleServiceError only recognizes axios errors, so the generic ServerError.

Changes

  • portal-service.ts: In generatePortal's catch, handle ApiError with status 401 — extract the API's message from the deserialized error result and return a ServiceError that pairs it with a suggested fix. All other errors continue to flow through handleServiceError unchanged (kept scoped on purpose; message handling differs per flow).

  • service-error.ts: New ServiceError.unauthorized(apiMessage) factory (alongside badRequest/forbidden/notFound) that appends "run apimatic auth login or provide a valid auth key using the --auth-key flag" 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

  • Reproduced end-to-end: portal serve with an empty config dir (logged out) against the production API — enhanced message shown.
  • Verified the axios 401 path is unaffected: auth status with an invalid stored key still prints "Invalid API key provided." (relies on the ServiceError.UnAuthorized singleton identity check, which is preserved).
  • pnpm test: 45 passing, lint clean.

…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]>
sohail2721 and others added 3 commits July 20, 2026 14:46
…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]>
@MuhammadRafay1
MuhammadRafay1 force-pushed the error-message-enhancement branch from 4325553 to 948e8aa Compare July 20, 2026 09:46

@sohail2721 sohail2721 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}

export function handleServiceError(error: unknown): ServiceError {
if (error instanceof ApiError) return mapApiError(error);

@mehnoorsiddiqui mehnoorsiddiqui Jul 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/infrastructure/service-error.ts Outdated
static notFound(customMessage: string): ServiceError {
return new ServiceError(ServiceErrorCode.NotFound, customMessage, {});
}
static unauthorized(apiMessage: string | null): ServiceError {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread src/infrastructure/service-error.ts Outdated
… 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]>
@sonarqubecloud

Copy link
Copy Markdown

@MuhammadRafay1
MuhammadRafay1 merged commit 376fd2b into dev Jul 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants