Skip to content

Client errors in the server log

Status: ✅ Implemented (Vaadin and Redwood renderers; every Java backend: Spring MVC, WebFlux, Micronaut, Quarkus, Helidon MP)

The user saw «Your session is no longer valid» — and nothing on the server says so. When the request never reached the app (a gateway rejected it, the network dropped it) or the failure was the browser’s own (an uncaught exception), the error only ever existed on one screen. You cannot debug what you cannot find.

Both renderers report the errors they hit or show to POST <baseUrl>/mateu/v3/client-log, on the same origin and with the same Authorization: Bearer … header as every other /mateu/v3 call — so a gateway routes and authenticates it like the rest. The backend writes one line per report on the logger mateu.client, at WARN:

WARN mateu.client : client-error {"level":"error","kind":"unauthorized","renderer":"redwood","message":"Tu sesión ya no es válida. Vuelve a iniciar sesión.","detail":"Mateu → HTTP 401","url":"/mateu/v3/sync/bookings","route":"/bookings","actionId":"save","pageUrl":"https://app.example.com/bookings","firstAt":"2026-10-05T14:02:11.120Z","lastAt":"2026-10-05T14:02:11.120Z","userAgent":"Mozilla/5.0 …","status":401,"count":1,"user":"ana"}

Nothing to configure: it is on by default. Turn it off with mateu.client-log.enabled=false (the endpoint then answers 404 and the renderers stop reporting for that page). The logger is a normal SLF4J logger, so logging.level.mateu.client=OFF silences the lines without touching the endpoint.

Sourcekind
A request failure the user is shown (the error toast / band)offline, timeout, server, unauthorized, forbidden, notFound, client, unknown
An uncaught error (window error)js-error
An unhandled promise rejectionunhandled-rejection
The request-loop circuit breaker tripping (Vaadin)request-loop

Never reported: cancelled (an abort is our own decision), failures of the report call itself (no loops), ResizeObserver loop noise.

level, kind, renderer (vaadin / redwood), message (what the user saw, ≤ 1000 chars), detail (the underlying error text), status, url (the failed request), route, actionId, pageUrl, source (file:line:col of a JS error), traceparent (when the request carried one), firstAt / lastAt / count, dropped, userAgent, stack (≤ 4000 chars), and user — the authenticated principal’s name, added by the server. URLs lose their #fragment, and query values such as code, state and access_token are masked as ***.

  • De-duplication. The same error (kind, status, message, url, action, first stack line) within a minute is one line: the first occurrence is sent after ~2 s; repeats are summed into one more line (count, firstAt, lastAt) when the minute closes or the page is hidden.
  • Rate limit. At most 20 lines a minute per page; lines over the limit are counted in the next line’s dropped.
  • Batching. Lines go out as a JSON array, kept under the server’s 16 KB body limit (413 above it).
  • Transport. fetch with keepalive and the Bearer token. sendBeacon cannot carry headers, so it is only the last resort on pagehide when there is no token.

Each pod’s stdout carries the lines, so with Alloy/Promtail shipping container logs:

{namespace="<ns>"} |= "client-error"

Narrow it down by parsing the JSON after the prefix:

{namespace="<ns>"} |= "client-error" | regexp `client-error (?P<report>\{.*\})` | line_format "{{.report}}" | json | kind="unauthorized"

Every Java adapter serves it on any path ending in /mateu/v3/client-log, with the same framework-neutral handling (io.mateu.core.infra.ClientErrorLog): a servlet filter on Spring MVC and a WebFilter on WebFlux (both ordered after Spring Security’s chain, so it is secured like the other Mateu calls), a dedicated route of the generated controller on Micronaut, and a route on the Vert.x router (Quarkus) or the Helidon routing (Helidon MP). Turn it off with mateu.client-log.enabled=false; the endpoint then answers 404, which the renderers read as “no endpoint here” and stop reporting for the page.