Summary
When apimatic sdk publish gets an auth failure from the publishing API, the CLI prints a message with no remedy in it — either the bare Unauthorized access. (401) or the generic An unexpected error occurred, please try again later… (403). Neither names apimatic auth login, so a user whose auth key has expired or been revoked has nothing to act on.
The 403 case is the worse of the two: an authorization failure is reported as an unexpected error, which reads like an APIMatic outage and points the user at support instead of at re-authenticating.
Reproduction
Build the CLI, then point it at a config with a rejected key:
pnpm build
mkdir -p /tmp/cfg && echo '{"email":"a@b.com","authKey":"bogus-invalid-key"}' > /tmp/cfg/config.json
cp -r test/resources/build-inputs/default /tmp/repro/src
APIMATIC_CONFIG_DIR=/tmp/cfg node ./bin/run.js sdk publish \
--input /tmp/repro --destination /tmp/repro/sdk --language typescript \
--profile-id a1b2c3d4e5f6a1b2c3d4e5f6 --version 1.0.0 --publish-type package --force
Actual (401 — verified against the live api.package-publishing.apimatic.io, which answers a bad key with 401 {"title":"Unauthorized","detail":"You are not authorized to access this resource"}):
■ Failed to search for publishing profile.
■ Unauthorized access.
└ Failed
Actual (403 — reproduced by pointing APIMATIC_BASE_URL's third segment at a local server returning 403 with a ProblemDetails body):
■ Failed to search for publishing profile.
■ An unexpected error occurred, please try again later. If the problem persists, please reach out to our team at 'support@apimatic.io'
└ Failed
Expected, matching what the SDK-generation step already prints for the same rejected key (apimatic sdk generate --auth-key bogus-invalid-key):
■ Authorization has been denied for this request. Please run apimatic auth login to log in via browser,
or provide a valid auth key using the apimatic auth login --auth-key
Cause
PublishingApiService routes every caught error through handleServiceError (src/infrastructure/services/publishing-api-service.ts:42, :102, :129), and that function is the one mapper that does not apply the auth hint:
handleServiceError maps an axios 401 onto the bare ServiceError.UnAuthorized singleton ("Unauthorized access."), not ServiceError.unauthorizedWithHint(...) — src/infrastructure/service-error.ts:169.
- It has no
403 branch at all for axios errors, so a 403 falls through to ServiceError.ServerError, whose message is "An unexpected error occurred…" (src/infrastructure/service-error.ts:18, :178).
Both gaps are already fixed in the wrappers that exist for exactly this purpose — mapTransportError (401 → hint) and mapRequestError (ProblemDetails 400/403 → the API's own title/detail). PluginService uses them (src/infrastructure/services/plugin-service.ts:94, :134, :154); PublishingApiService does not.
#300 (a1ab840) fixed only the local pre-check in this service — the "no key in config.json" branch, which correctly returns unauthorizedWithHint(null). A user who has a key that the server rejects still takes the wire path, which was left on handleServiceError. That's why logging out gives a good message but an expired key does not.
Suggested fix
Swap the three handleServiceError calls in PublishingApiService for mapRequestError, which handles ProblemDetails 400/403 bodies and the 401 hint in one step. The hand-rolled error.status === 400 / 403 / 404 block in publishSdkPackage (:85–:100) then becomes largely redundant — and it is fragile anyway: it dereferences data.errors / data.title without checking that data is an object, so an error response with an empty or non-JSON body throws a TypeError out of the catch.
Longer term, handleServiceError itself should stop being the odd mapper out — the two "correct" behaviours living in wrappers around it means every new call site has a 50/50 chance of picking the one that drops the remedy.
Related, same flow
pollPublishingStatus discards the ServiceError entirely — a 401 while polling getSdkPublishingLog prints only Failed to fetch publishing status. (src/prompts/sdk/publish.ts:60-62). Fixing the mapper above will not surface anything there until the prompt logs publishingLogResult.error.errorMessage.
Also worth a look: the SDK/portal generation status pollers (getSdkGenerationStatus, getV4SdkGenerationStatus, getPortalGenerationStatus in portal-service.ts) classify 401 as the same bare ServiceError.UnAuthorized, and everything unrecognised as ServiceError.InvalidResponse — which carries that identical "An unexpected error occurred…" text. The status endpoints do answer a rejected key with 401 (verified), so a key that expires mid-generation lands on the bare message too.
Summary
When
apimatic sdk publishgets an auth failure from the publishing API, the CLI prints a message with no remedy in it — either the bareUnauthorized access.(401) or the genericAn unexpected error occurred, please try again later…(403). Neither namesapimatic auth login, so a user whose auth key has expired or been revoked has nothing to act on.The 403 case is the worse of the two: an authorization failure is reported as an unexpected error, which reads like an APIMatic outage and points the user at support instead of at re-authenticating.
Reproduction
Build the CLI, then point it at a config with a rejected key:
Actual (401 — verified against the live
api.package-publishing.apimatic.io, which answers a bad key with401 {"title":"Unauthorized","detail":"You are not authorized to access this resource"}):Actual (403 — reproduced by pointing
APIMATIC_BASE_URL's third segment at a local server returning403with a ProblemDetails body):Expected, matching what the SDK-generation step already prints for the same rejected key (
apimatic sdk generate --auth-key bogus-invalid-key):Cause
PublishingApiServiceroutes every caught error throughhandleServiceError(src/infrastructure/services/publishing-api-service.ts:42,:102,:129), and that function is the one mapper that does not apply the auth hint:handleServiceErrormaps an axios401onto the bareServiceError.UnAuthorizedsingleton ("Unauthorized access."), notServiceError.unauthorizedWithHint(...)—src/infrastructure/service-error.ts:169.403branch at all for axios errors, so a 403 falls through toServiceError.ServerError, whose message is"An unexpected error occurred…"(src/infrastructure/service-error.ts:18,:178).Both gaps are already fixed in the wrappers that exist for exactly this purpose —
mapTransportError(401 → hint) andmapRequestError(ProblemDetails 400/403 → the API's own title/detail).PluginServiceuses them (src/infrastructure/services/plugin-service.ts:94,:134,:154);PublishingApiServicedoes not.#300 (
a1ab840) fixed only the local pre-check in this service — the "no key inconfig.json" branch, which correctly returnsunauthorizedWithHint(null). A user who has a key that the server rejects still takes the wire path, which was left onhandleServiceError. That's why logging out gives a good message but an expired key does not.Suggested fix
Swap the three
handleServiceErrorcalls inPublishingApiServiceformapRequestError, which handles ProblemDetails 400/403 bodies and the 401 hint in one step. The hand-rollederror.status === 400 / 403 / 404block inpublishSdkPackage(:85–:100) then becomes largely redundant — and it is fragile anyway: it dereferencesdata.errors/data.titlewithout checking thatdatais an object, so an error response with an empty or non-JSON body throws aTypeErrorout of thecatch.Longer term,
handleServiceErroritself should stop being the odd mapper out — the two "correct" behaviours living in wrappers around it means every new call site has a 50/50 chance of picking the one that drops the remedy.Related, same flow
pollPublishingStatusdiscards theServiceErrorentirely — a 401 while pollinggetSdkPublishingLogprints onlyFailed to fetch publishing status.(src/prompts/sdk/publish.ts:60-62). Fixing the mapper above will not surface anything there until the prompt logspublishingLogResult.error.errorMessage.Also worth a look: the SDK/portal generation status pollers (
getSdkGenerationStatus,getV4SdkGenerationStatus,getPortalGenerationStatusinportal-service.ts) classify401as the same bareServiceError.UnAuthorized, and everything unrecognised asServiceError.InvalidResponse— which carries that identical"An unexpected error occurred…"text. The status endpoints do answer a rejected key with401(verified), so a key that expires mid-generation lands on the bare message too.