Skip to main content

Reference · Errors

HTTP status codes the Neuroon API can return and the body shape. Most responses come from GlobalExceptionHandler (api/src/main/java/.../rest/GlobalExceptionHandler.java); exceptions are documented below.

Standard envelope

{
"timestamp": "2026-05-06T10:15:00",
"status": 400,
"error": "Bad Request",
"message": "field-level error joined with commas",
"path": "/api/widget/search"
}

Built in GlobalExceptionHandler.java:147-156.

FieldTypeNotes
timestampstringLocalDateTime.now().toString() (server runs in UTC, no explicit zone).
statusnumberHTTP code.
errorstringReason phrase for the status (Bad Request, Unauthorized, …).
messagestringDetail. On 400 it concatenates validation errors with , .
pathstringRequest path stripped of the uri= prefix WebRequest adds.

HTTP codes emitted by the global handler

StatusJava exception that triggers itMeaning
400 Bad RequestMethodArgumentNotValidException, ConstraintViolationException, MissingServletRequestParameterException, IllegalStateException, IllegalArgumentExceptionValidation failure (@Valid, missing @RequestParam, domain rule).
401 UnauthorizedBadCredentialsExceptionInvalid credentials.
403 ForbiddenAccessDeniedExceptionToken not authorized for the resource.
404 Not FoundResponseStatusException(NOT_FOUND) thrown from a controllerMissing resource (shopId, product).
500 Internal Server ErrorException (catch-all)Unhandled exception. Report with timestamp and path.
502 Bad GatewayPaymentGatewayExceptionStripe unreachable.
503 Service UnavailableCallNotPermittedException (open circuit breaker)External provider in open state. Retry with backoff.

Other statuses (405, 409, 415, …) are emitted by Spring directly without going through the handler — the body uses Spring's default envelope ({ timestamp, status, error, path }) without message.

Special cases (different envelope)

429 Too Many Requests — rate limit

Emitted by the filters (WidgetRateLimitFilter, RateLimitFilter), not by the global handler. The envelope is different:

{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again later.",
"retryAfter": 60,
"limit": 200,
"remaining": 0,
"timestamp": "2026-05-06T10:15:00Z"
}

Built in WidgetRateLimitFilter.java:90-93. Associated headers: Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining. See Rate Limits.

401 / 403 from ApiKeyAuthenticationFilter

When the Shop API Key fails validation, the auth filter emits directly without going through the global handler:

{
"error": "Invalid or missing X-Shop-API-Key header",
"status": 401
}

At ApiKeyAuthenticationFilter.java:131. Same shape with status: 403 when the origin is not authorized.

Validation errors (400)

MethodArgumentNotValidException and ConstraintViolationException are joined into message with the format field: description:

{
"timestamp": "2026-05-06T10:15:00",
"status": 400,
"error": "Bad Request",
"message": "products[0].externalId: must not be blank, products[0].price: must be positive",
"path": "/api/plugin/shops/123/products/sync"
}

GlobalExceptionHandler.java:22-47.

Best practices

  • Don't regex-parse message — the format is not stable.
  • Retry only on 429, 502, 503 with exponential backoff + jitter.
  • Don't retry on 400, 401, 403, 404 — those are client failures.
  • Log timestamp, path and, if available, requestId to correlate with the server side when opening a ticket.
  • Distinguish the two envelopes — most use { timestamp, status, error, message, path }; rate-limit and auth filters use their own formats.

Further reading