Summary
HttpClient.handleResponse() throws HaloPsaAuthenticationError("Bad request - invalid credentials or parameters") for any HTTP 400 response that isn't in the recognized {errors: [...]} / {validation_errors: [...]} shape — regardless of whether the 400 has anything to do with credentials. This misleads API consumers (and end users reading the resulting error) into thinking a 400 is an auth/permissions problem when it's actually just a malformed or incomplete request payload.
https://github.com/WYRE-AI/node-halopsa/blob/main/src/http.ts#L164-L174
switch (response.status) {
case 400:
// Could be bad credentials on token request or validation error
if (this.isValidationError(responseBody)) {
const errors = this.parseValidationErrors(responseBody);
throw new HaloPsaValidationError('Validation error', errors, responseBody);
}
throw new HaloPsaAuthenticationError(
'Bad request - invalid credentials or parameters',
400,
responseBody
);
Why this is a real problem
The comment (// Could be bad credentials on token request or validation error) shows this was written to cover two genuinely different cases with one branch:
- OAuth token endpoint (
/token) — HaloPSA follows the OAuth spec here, where a bad client_id/client_secret legitimately comes back as 400 invalid_client/invalid_grant, not 401. HaloPsaAuthenticationError is the right type here.
- Any other API endpoint (e.g.
POST /Actions, POST /Tickets) — credentials are already validated via the Bearer token (401 is the auth-failure signal for these). A 400 here means the request body itself was rejected — a missing required field, an invalid enum value, etc. This has nothing to do with credentials, but gets the identical HaloPsaAuthenticationError + "invalid credentials or parameters" message.
We hit this in production via halopsa-mcp: a customer's POST /Actions call (adding a ticket note) failed with a plain, non-errors-shaped 400 body, so it surfaced to the caller as "Bad request - invalid credentials or parameters". The customer read that as "my API application's permissions/credentials must be wrong," spent time reviewing their HaloPSA Agent/Application Identity scopes and considering provisioning a new dedicated API user — none of which was the actual problem. The request just needed a field HaloPSA's server-side validation requires that isn't marked required in ActionCreateData.
Suggested fix
Distinguish by request context rather than guessing from the body shape alone:
- For the token/auth request path, keep throwing
HaloPsaAuthenticationError on a 400 (that's accurate there).
- For all other endpoints, a non-validation-shaped 400 should throw something neutral — e.g. a
HaloPsaBadRequestError (or plain HaloPsaError) with a message like "Bad request (400): <endpoint> rejected the request parameters" — not an HaloPsaAuthenticationError, and not the word "credentials" at all.
Happy to send a PR if that approach sounds right — wanted to confirm intended error taxonomy first since HaloPsaAuthenticationError vs a new HaloPsaBadRequestError is a public API surface change for consumers catching specific error classes.
Related
Distinct from #76 (now fixed) — that was about the response shape on success; this is about error classification on a 400 status.
Summary
HttpClient.handleResponse()throwsHaloPsaAuthenticationError("Bad request - invalid credentials or parameters")for any HTTP 400 response that isn't in the recognized{errors: [...]}/{validation_errors: [...]}shape — regardless of whether the 400 has anything to do with credentials. This misleads API consumers (and end users reading the resulting error) into thinking a 400 is an auth/permissions problem when it's actually just a malformed or incomplete request payload.https://github.com/WYRE-AI/node-halopsa/blob/main/src/http.ts#L164-L174
Why this is a real problem
The comment (
// Could be bad credentials on token request or validation error) shows this was written to cover two genuinely different cases with one branch:/token) — HaloPSA follows the OAuth spec here, where a badclient_id/client_secretlegitimately comes back as400 invalid_client/invalid_grant, not 401.HaloPsaAuthenticationErroris the right type here.POST /Actions,POST /Tickets) — credentials are already validated via the Bearer token (401 is the auth-failure signal for these). A 400 here means the request body itself was rejected — a missing required field, an invalid enum value, etc. This has nothing to do with credentials, but gets the identicalHaloPsaAuthenticationError+ "invalid credentials or parameters" message.We hit this in production via
halopsa-mcp: a customer'sPOST /Actionscall (adding a ticket note) failed with a plain, non-errors-shaped 400 body, so it surfaced to the caller as"Bad request - invalid credentials or parameters". The customer read that as "my API application's permissions/credentials must be wrong," spent time reviewing their HaloPSA Agent/Application Identity scopes and considering provisioning a new dedicated API user — none of which was the actual problem. The request just needed a field HaloPSA's server-side validation requires that isn't markedrequiredinActionCreateData.Suggested fix
Distinguish by request context rather than guessing from the body shape alone:
HaloPsaAuthenticationErroron a 400 (that's accurate there).HaloPsaBadRequestError(or plainHaloPsaError) with a message like"Bad request (400): <endpoint> rejected the request parameters"— not anHaloPsaAuthenticationError, and not the word "credentials" at all.Happy to send a PR if that approach sounds right — wanted to confirm intended error taxonomy first since
HaloPsaAuthenticationErrorvs a newHaloPsaBadRequestErroris a public API surface change for consumers catching specific error classes.Related
Distinct from #76 (now fixed) — that was about the response shape on success; this is about error classification on a 400 status.