Logging / Debugging
REST request/response debug capture is driven by a single signal: the resource's own java.util.logging.Logger
level. There's nothing Juneau-specific to configure — the same logging.properties file (or Logger.setLevel(...)
call) an operator already uses to turn up verbose logging for a class also turns on REST header/body capture for
that class's endpoints, at three cumulative tiers.
Overview
- One signal. No debug annotations, no on-demand per-request headers, no separate logging SPI to wire up. The resolved JUL logger's level is the sole control.
- Cumulative tiers.
INFO= basic request/response logging,FINE= adds headers,FINEST= adds bodies — but only when body dumping has been explicitly opted in via theJUNEAU_REST_DEBUG_ALLOW_DUMP_BODIESenvironment variable; otherwiseFINESTemits a suppression placeholder instead of body content. Each tier includes everything the tier below it logs. - One log record per request, always emitted at JUL
INFO. The resolved tier controls content only. - Secure by default. Bodies are never dumped unless an operator sets an environment-variable master gate; every value of every credential-bearing header is masked; every client-controlled string is escaped against log-forging; and all output is bounded — see Secure by default below.
Level resolution
Two JUL loggers are in play for any given request:
- Resource logger — the resource's resolved logger:
Logger.getLogger(resourceClass.getName())by default, or whatever a@Bean-annotatedRichLoggerfactory method overrides it to (see Bean-overridden resource loggers below). - Operation logger —
Logger.getLogger(resourceLoggerName + "." + methodName), once the request has been routed to a specific@RestOpmethod, whereresourceLoggerNameis the resolved resource logger's name from above (i.e. the operation logger is always a JUL child of the resolved resource logger, override or not).
Juneau resolves the operation logger first, falling back to the resource logger on unrouted/404 paths. Because this is a plain java.util.logging.Logger hierarchy, ordinary JUL parent-inheritance rules apply — an operation logger with no level of its own inherits the resource logger's level.
Default resource logger naming normalizes common proxy-shaped classes (Spring CGLIB, ByteBuddy, JDK proxy-style, Hibernate/Mockito proxy infixes) back to the user class name, while preserving ordinary nested class names unchanged. This avoids logger-name churn when resources are wrapped by runtime proxies.
// logging.properties
com.example.MyResource.level = FINE
com.example.MyResource.getWidget.level = FINEST
With the configuration above:
- Every operation on
MyResourcelogs at least basic + headers (inheritedFINEfrom the resource logger). getWidget()specifically also logs bodies (FINESTset directly on its own operation-level child logger).
This is exactly the "set the resource logger to FINE, elevate one method to FINEST" pattern — per-operation
control falls out of ordinary JUL inheritance, no Juneau-specific per-method attribute required.
Mixin operations resolve against the host
An operation contributed by a composed @Rest(mixins=...) mixin uses
the host / top-level resource that composed the mixin for its resource-logger portion — not the mixin class
itself:
public class EchoMixin { @RestGet(path="/echo/*") public ... echo(...) {...} }
@Rest(mixins=EchoMixin.class)
public class MyResource extends RestServlet {...}
Here the mixin-served echo operation's logger is named com.example.MyResource.echo, not
com.example.EchoMixin.echo. Consequences:
- Raising
com.example.MyResource's own JUL level cascades toecho(and every other mixin-served operation) — ordinary JUL parent inheritance, same as any other operation on the host. - The same mixin class composed into two different host resources resolves to two independent, host-isolated
operation loggers (
HostA.echoandHostB.echo) — raising one host's level never leaks into the other.
Bean-overridden resource loggers
If a resource replaces its resolved logger via a @Bean-annotated RichLogger factory method:
public class MyResource extends RestServlet {
@Bean
public RichLogger logger() {
return RichLogger.getLogger("my.app.custom.logger.name");
}
}
every operation logger on that resource is named as a child of the overridden name
(my.app.custom.logger.name.<methodName>), not the raw resource class name — so raising the overridden logger's
level still cascades to its operations exactly as it would for the default, class-named logger.
404 / unrouted requests
When no operation is resolved (the request 404s, or otherwise never reaches a matched @RestOp method), Juneau
falls back to the resource logger, but only ever emits the basic status-line summary — headers and bodies are
never rendered on this path, no matter how high the resolved logger's level is set. Raising a resource to FINEST
makes its operations log full detail; it does not retroactively add header/body detail to 404s.
Tier table
| Level | Content |
|---|---|
below INFO, or INFO inherited from the root logger | Nothing emitted. Stock JUL leaves the root at INFO, so a level must be set explicitly on the resource's logger (or an enclosing logger) to turn the line on. |
INFO (set explicitly) | Basic request/response summary (method, path, status, timing). |
FINE | Basic + request/response headers (every value of every credential-bearing header masked). |
FINEST | Basic + headers + request/response body content — only if JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES is set (and the content is renderable); otherwise a suppression placeholder + byte count. |
FINER resolves through the FINE tier (headers), while CONFIG resolves through INFO (basic line only), matching
JUL isLoggable(...) semantics used by the pipeline.
The tier is re-derived at request completion via isLoggable(...) against the resolved logger, and is stable for
the life of the request. Content is cumulative — FINEST always includes everything FINE and INFO log, in one
LogRecord.
Two-phase capture (internal)
The two-phase pipeline below is described for context only. It is not an extension point; the only supported
customization surface is the RestDebugFormatter SPI.
- Phase A (request start): the operation logger is resolved as soon as routing completes. If it's loggable at
FINEST, bounded body-caching wrappers are installed around the request/response streams; otherwise no wrapping happens at all, so requests belowFINESTpay no buffering overhead. - Phase B (request completion): the tier is re-derived, the resolved
RestDebugFormatteris invoked cumulatively (formatBasicalways,+ formatHeadersatFINE-or-finer,+ formatBodyatFINEST), header redaction and log-forging sanitization are applied, and exactly oneLogRecordis emitted atINFOthrough the resolved logger.formatBodyonly ever emits actual body bytes when theJUNEAU_REST_DEBUG_ALLOW_DUMP_BODIESgate is set (see Secure by default); otherwise it emits a placeholder. - Bounded capture. The caching wrapper buffers at most the configured body cap (default 8 KB), not the entire stream, so memory use is bounded even for large uploads/downloads.
- Edge case: if the logger's level is changed during a request (between Phase A and Phase B), the two phases can disagree. The only symptom is a body that wasn't cached — Phase B simply degrades to headers-only for that one request. This isn't a correctness bug, just a best-effort consequence of resolving the tier twice.
When Phase B runs for async and reactive responses
Phase B emission timing depends on whether a request obtains a real servlet AsyncContext:
- Synchronous responses (and any request handled through the
MockRestClient/ non-async-container fallback) emit their single record during synchronous request finish, exactly as before. - True asynchronous responses (
CompletableFuture/CompletionStagereturn types) emit on the response-completion thread, after the async body and any headers set during completion have been written — not during the synchronous finish of the request thread. The resolved logger/formatter/detail-tier is snapshotted on the request thread at the async hand-off and carried across the completion hop, so completion-path rendering never re-resolves state from foreign-thread context. This fixes the previous behavior where an async record could be emitted before its late headers/body existed, capturing an incomplete picture. Finish-time attributes such asExec timeare recorded on the completion path so they reflect the true request duration. - Reactive SSE / NDJSON streams emit once after the stream terminates (normal completion or error), through the
same completion path. At
FINESTthe captured stream prefix is bounded by the body cap and shows a visible truncation marker when the stream exceeds it.
Regardless of timing, exactly one LogRecord is emitted per request, always at INFO (the resolved tier controls
message detail only). Note that @RestEndCall methods still run on the request thread at lifecycle time (before the
async body completes); completion-path emission is a debug-pipeline detail, not a general lifecycle migration.
The RestDebugFormatter SPI
RestDebugFormatter is the one public extension point for how a captured request/response is rendered into a log message:
package org.apache.juneau.rest.server.logging;
public interface RestDebugFormatter {
String formatBasic(RestRequest req, RestResponse res); // INFO tier
default String formatHeaders(RestRequest req, RestResponse res) {...} // added at FINE
default String formatBody(RestRequest req, RestResponse res) {...} // added at FINEST (gated)
default String statusLine(HttpServletRequest req, HttpServletResponse res) {...} // sanitized + URI-capped
default boolean isBodyRenderable(String contentType) {...} // text-vs-binary predicate
}
Override only the tier(s) you care about — the other tiers keep the shipped defaults.
Default implementation
BasicRestDebugFormatter
ships working defaults for all three tiers and owns the header-redaction set, the body-dump gate, the
RestDebugBodyScrubber hook, and the output bounds (see Secure by default below). It's the
zero-configuration default — you don't need to register anything to get useful, secure debug output.
Resolution
A resource's formatter resolves in the following precedence order:
- Resource class implements
RestDebugFormatter— the resource itself is used directly. BeanStore-registered bean — a@Bean-annotated method/field (or, in Spring Boot, a Spring@Bean) supplying aRestDebugFormatter.BasicRestDebugFormatter— the shipped default.
// Option 1 - resource implements the SPI directly
public class MyResource extends BasicRestServlet implements RestDebugFormatter {
@Override
public String formatBody(RestRequest req, RestResponse res) {
return "custom body rendering...";
}
}
// Option 2 - supply a formatter bean
public class MyResource extends BasicRestServlet {
@Bean
public RestDebugFormatter debugFormatter() {
return new MyCustomFormatter();
}
}
Secure by default
BasicRestDebugFormatter is designed so that raising a resource/operation logger to FINE/FINEST can never, by
itself, turn the log stream into a plaintext-credential dump or a log-forging channel controlled by a remote
client. Four independent protections apply.
No body dumps without an explicit opt-in
Request/response bodies are never dumped by default — not at FINEST, not for any content type. Dumping is a
deliberate operator opt-in behind a single environment-variable master gate:
export JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES=true
Semantics:
- Environment variable only. There is no system-property fallback. It is read once per JVM and cached.
- Trim-then-parse truthy. The value is trimmed, then any non-empty value enables except case-insensitive
falseor0, which disable. Unset, empty, or all-whitespace disables. This is a fail-safe kill switch:" false "and" "both resolve to disabled and never silently enable dumping. - Gate off ⇒ placeholder only. With the gate unset,
formatBodyemits only a suppression placeholder plus the captured byte count and content-type — never body bytes in any representation — even if a scrubber is configured. - Gate on ⇒ only at
FINEST, only on renderable content. Even opted in, bodies render only at theFINESTtier and only when the content is renderable text (see below). Non-renderable/binary content yields a distinct "not rendered" placeholder.
Setting JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES re-enables plaintext request/response body logging at FINEST. Treat
FINEST logs as sensitive: restrict their sink, and supply a RestDebugBodyScrubber if any
secret-bearing bodies (tokens, PII, card data) are possible. Prefer leaving the gate unset in production.
Renderable-content predicate
isBodyRenderable(String contentType) decides whether a body is text worth logging. It parses the media type
(ignoring parameters such as ; charset=utf-8) and treats as renderable: text/*, application/json,
application/xml, any +json/+xml suffix type, and application/x-www-form-urlencoded. Everything else —
including multipart/form-data, absent, or blank content types — is non-renderable. A non-identity
Content-Encoding (e.g. gzip) is also treated as non-renderable. The decision is made per side: the request
content type governs the request body, the response content type governs the response body.
Body scrubbing SPI
When body dumping is opted in, you can transform body text before it is logged via the
RestDebugBodyScrubber
SPI (String scrub(String contentType, String body)), registered with bodyScrubber(...):
- It is gate-subordinate: a scrubber is invoked only after the master gate has already permitted dumping. It merely chooses scrubbed-vs-raw text; it never causes a body to be emitted while the gate is unset.
- It is fail-closed: if
scrub(...)throws or returnsnull, the formatter emits a suppression placeholder instead of the body — it never falls back to the raw, unscrubbed body. - Non-
nulloutput is still sanitized (control characters escaped) and length-capped before it reaches the log. - Implementations must be thread-safe — the formatter may call
scrub(...)concurrently.
No default scrubber ships: with the gate set and no scrubber, the raw body is dumped (sanitized + capped).
Header redaction — all values, widened set
At the headers tier (FINE-or-finer), every value of every credential-bearing header is masked with
[REDACTED] (multi-valued headers such as repeated Set-Cookie are masked value-by-value, not just the first).
The formatter matches against a widened, formatter-local set — a superset of the shared
RedactedHeaders.DEFAULT (Authorization, Cookie, Set-Cookie, Proxy-Authorization, X-API-Key) plus
X-Auth-Token, X-Authorization, WWW-Authenticate, Referer, and Location. Matching is
separator-normalized: it case-folds and strips -/_, so X-Auth-Token, X_Auth_Token, and XAuthToken
all match. The shared RedactedHeaders.DEFAULT itself is left unchanged.
Configure the masked set with:
redactedHeaders(Collection<String>)— replaces the masked set.addRedactedHeaders(Collection<String>)— adds to the built-in widened set without dropping the built-ins.
Log-forging sanitization
Every client-controlled string — the request method and URI (including on the 404 / unrouted path), header names
and values, and body text (raw or scrubber-produced) — is passed through a sanitizer that escapes CR/LF and other
control characters (NEL, line/paragraph separators, bidi controls, C0/C1 controls; TAB is preserved) to visible,
inert forms (\r, \n, \uXXXX). A remote client therefore cannot inject a newline to forge a second physical
log line or a fake status banner. The order applied to every emitted string is mask → escape → cap.
Bounded output
All output axes are bounded so a flood cannot exhaust the log sink:
| Bound | Knob | Default |
|---|---|---|
Captured body bytes (Phase A capture time), then …[truncated N bytes] | bodyCap(int) | 8 KB |
Emitted header values per block, then …[+K more headers omitted] | maxHeaders(int) | 100 |
| Header values scanned while counting omissions | maxHeaderScan(int) | 1000 |
| Per header name/value length (after masking/escaping) | maxFieldLength(int) | 1024 |
| Rendered URI length | maxUriLength(int) | 2048 |
The body byte cap is enforced at capture time (Phase A above), so oversized bodies never sit fully buffered in
memory; a second character-level cap is applied after sanitization so a scrubber that returns a huge string is
still bounded. There is no separate body-dump cap knob — bodyCap() is the single source of truth.
See the migration guide's footgun callout for operational guidance on raising logger levels in production.
Using Log4j for logging
The REST API uses Java logging by default.
If you wish to use LOG4J logging, you simple need to add the following to your JVM arguments and maven dependencies:
Command-line argument
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
Maven dependency
log4j-jul is only the bridge that routes java.util.logging calls into Log4j2 — it does not include a Log4j2 implementation. You also need log4j-core (or log4j-to-slf4j plus an SLF4J binding) on the classpath, or nothing will actually process the routed log records:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
Management Logging — Logback & Log4j2 Backends covers the more thorough juneau-rest-server-management-logging module, which lets the /loggers runtime management endpoint drive Logback or Log4j2 directly instead of just redirecting JUL output.
Application logging from a handler
Separately from the one cumulative debug record the pipeline emits per request, handler code can emit its own ad hoc log
statements during request processing via convenience methods on
RestRequest. These delegate to
the request's resolved per-operation RichLogger (req.getLogger()), so they ride the same JUL level resolution and
the same parent-chain capture propagation as everything else on that logger:
@RestGet(path="/widgets/{id}")
public Widget getWidget(RestRequest req, @Path("id") String id) {
req.fine("Loading widget %s", id);
try {
return widgetService.load(id);
} catch (NotFoundException e) {
req.warning(e, "Widget %s not found", id);
throw e;
}
}
- A full JUL-level ladder is provided —
severe/warning/info/config/fine/finer/finest— each with a(String pattern, Object...args)form and a(Throwable thrown, String pattern, Object...args)overload. Patterns arejava.util.Formatter-style. - These are complementary to, not a replacement for, the single cumulative debug record; they produce independent,
additional
LogRecords. req.getLogger()is exposed directly if you need theRichLoggeritself.
Correlating records with a request-scoped LogContext
While a thread-confined
LogContext scope is open on the request thread,
every record emitted on that thread — the pipeline's debug record and any handler-emitted convenience-method records
above — automatically carries the scope's entries, retrievable via LogRecordContext.of(record).get(key):
@RestGet(path="/widgets/{id}")
public Widget getWidget(RestRequest req, @Path("id") String id) {
try (var s = RichLogger.context().with("widgetId", id)) {
req.fine("loading"); // this record carries widgetId
return widgetService.load(id);
}
}
For true asynchronous responses, the request-thread LogContext snapshot is captured at the async hand-off and
re-applied to the completion-path debug record, so that record carries the same entries even though it is emitted on a
foreign thread whose live LogContext is empty. Note that ad hoc logging performed directly from an async-completion
callback (not through the debug pipeline) does not carry the context in v1 — see the thread-confinement note in
LogContext.
Always-on request correlation (requestId)
The framework opens exactly one requestId LogContext
scope per request, during session build — before any @RestStartCall hook runs. Because it is anchored at session
build rather than at a hook, it covers every path that produces a debug record, including 404s and other
early-error paths that never reach @RestStartCall. The scope is closed at the very end of request processing, after
the debug record is emitted (emit-before-close), so the emitted record always carries the id.
The per-request id is resolved mint-or-honor: an inbound X-Request-Id request header is honored if present,
otherwise a fresh id is minted (a Uuid7, time-ordered). The honored value runs
through a validator that defaults to sanitize-and-accept — control characters and log-forging sequences are
stripped and the (bounded) remainder is accepted, rather than rejected-and-reminted — so a caller-supplied id survives
end-to-end whenever it is safe to log. The resolved id is echoed back on the X-Request-Id response header and is
available synchronously to handler code via RestRequest.getRequestId().
Resolution behavior is configurable through a RequestIdSettings bean resolved by RestContext (a @Bean factory
method or default is picked up automatically):
validator(Predicate<String>)— replace the default sanitize-and-accept policy (e.g. a strict allowlist regex).attributeKey(String)— move the servlet-request attribute the resolved id is published under.idSupplier(Supplier<String>)— replace the mint source.
Every record emitted on the request thread — the pipeline's cumulative debug record and any handler-emitted
convenience-method records — automatically carries the scope's requestId entry (LogRecordContext.of(record).get("requestId")).
The statusLine of the basic debug formatter additionally prepends a human-readable [requestId=<id>] marker to the
rendered message. For true asynchronous responses, the id rides the same request-thread LogContext snapshot
described above, so the completion-path record carries the identical requestId in both its structured field and its
rendered prefix even though it is emitted on a foreign thread.
The pluggable request-id filter no longer owns the lifecycle — the always-on session-build resolver above is the
primary correlation mechanism. The filter is retained as a thin configuration façade whose apply() is idempotent
with respect to the already-resolved session id (it honors, it never double-mints); its per-instance builder knobs
(idSupplier/validator/attributeKey) are documented no-ops — configure request-id behavior on
RequestIdSettings instead.
Interaction with marshall-layer debug
REST logger levels now control logging verbosity only — they no longer change marshalling behavior.
RestRequest.isDebug() still exists as a derived, read-only logger query (true when the resolved logger is at
FINE-or-finer), but the framework no longer keys any behavior off it.
Serializer/parser debug behavior — recursion detection during serialization, richer/stack-prefixed serialization
exceptions, and retained/quoted parse input in error messages — is now controlled by a dedicated cascading
debugMarshalling setting that defaults OFF at every log level:
@Rest(debugMarshalling="true") // resource-wide
public class MyResource extends RestServlet {
@RestGet(path="/a", debugMarshalling="true") // op opts in
public MyBean a() {...}
@RestGet(path="/b") // blank ("") ⇒ inherit resource setting
public MyBean b() {...}
@RestGet(path="/c", debugMarshalling="false") // op opts out even if the resource is on
public MyBean c() {...}
}
debugMarshalling is a String on all eight REST annotation types (@Rest, @RestOp, and the six HTTP-method
annotations), so a blank "" value means inherit (op inherits the resource, which resolves the
${juneau.debugMarshalling:false} default). It is queryable at runtime via RestRequest.isDebugMarshalling(). A
serializer/parser configured with its own marshall-layer Context.debug remains independent — the REST layer only
forwards its debugMarshalling decision and never clobbers a directly-set Context.debug.
EchoMixin reachability
The EchoMixin echo-back endpoint is disabled by default and returns
404 Not Found regardless of logger level. Raising a logger to FINE/FINEST no longer unhides it — reachability
is an explicit, non-logging decision:
@Bean public EchoMixin echo() {
return EchoMixin.create().enabled().build(); // or .enabled(true)
}
Unset enablement resolves the ${juneau.echo.enabled:false} SVL fallback (system property / relaxed environment
variable), and an explicit enabled(false) always wins over that default. Because that fallback is a JVM-global
signal, prefer per-host builder enablement so one host's echo doesn't silently reach across every resource that
mixes EchoMixin in; pair it with the host's inherited @Rest(guards=...) chain for authorization. See
Ops / Introspection Mixin Pack for the full behavior, including the
sensitive-header redaction that applies to the echoed-back request as well.
Testing
Capture debug output by logger name with RichLogger:
try (var c = RichLogger.getLogger(MyResource.class).captureEvents(Level.FINEST)) {
client.get("/who").run();
assertTrue(c.last().getMessage().contains("[200] HTTP GET /api/who"));
}
captureEvents(Level.FINEST) is the right threshold for .debug()/logLevel(FINEST) mock flows, since debug
records are emitted at INFO, and the capture threshold is >=-based. captureEvents(Level.INFO) is the precise
threshold; captureEvents(Level.FINEST) still captures these records for existing tests. Pair this with the mock
request's logLevel(Level) helper to set the resolved
logger level for one call (and restore it afterward) — the mock equivalent of editing logging.properties without
introducing a separate boolean debug flag.
Keeping expected-error stack traces out of the log
When a request fails, the per-request record carries the exception behind the failure as its thrown, so a test
that provokes a 404, 405, 415 or other framework-level error on purpose prints a stack trace for an outcome it
expects. Send No-Trace: true, or set the NoTrace request attribute (RestRequest.setNoTrace() on the server),
to leave the stack trace off that record. The one-line summary (status, method, path) is still logged.
- If both the header and the attribute are present, the attribute wins (so
NoTrace=falsebeatsNo-Trace: true). - The setting is opt-in per request; nothing turns it on by default.
- It affects this per-request record only. Errors your own code logs, and the
FINEexception log written byRestContext.handleError, are not changed.
In tests, use the mock client's noTrace(); see
NG Mock REST Client.
Spring/back-end notes
- Spring Boot + Logback can propagate
logging.level.*into JUL throughLevelChangePropagatorwhen enabled. - The propagator's inherit-then-parent-change synchronization hole still applies: a JUL child logger set to inherit can remain stale after a later parent-level change.
- For nested resource class names that contain
$,logging.level.<fqcn>property keys may need escaping in Spring property sources because$...can be interpreted as a placeholder. - Backend fallback matrix:
- Logback: auto-propagation support.
- Log4j2 +
log4j-jul: JUL facade path works. - Log4j2 without
log4j-jul: no JUL back-propagation. - Plain JUL / other SLF4J backends: configure JUL directly.
Stable-INFO emission means FINE/FINEST-selected detail travels through INFO sinks. Headers are always masked
and bodies are suppressed by default (see Secure by default), but a broad FINEST raise on a
production resource logger still emits full header names and metadata through INFO — and, if
JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES is set, plaintext bodies. Raise levels deliberately and keep the body-dump
gate unset in production.
Migrating from pre-10.0
If you're coming from a pre-10.0 REST debug/logging setup — @Rest(debug=...), @Debug, DebugEnablement,
CallLogger, or the juneau.restLogger.* config keys — none of that exists anymore. See the
V10 Migration Guide's REST debug/logging redesign section
for the full removed-symbol mapping and a prominent warning about one operational footgun worth reading before you
flip a resource's logger to FINE/FINEST.
Related: exception logging (ThrownStore)
Separately from request/response debug capture, thrown exceptions are logged with a stack-trace hash and an occurrence counter:
WARNING: [500,9b85cc96.13] HTTP POST /foo?foo=bar
This is controlled by the ThrownStore
bean, an orthogonal mechanism unaffected by the logging model described above. It's resolved the same way as other
framework beans — via a @Bean(name="thrownStore")-annotated method/field, or (in Spring Boot) a Spring @Bean.
One case worth knowing: a BeanQueryExecutionException (an engine-side failure in a Queryable or BeanQuery-backed operation, as opposed to a client-caused BeanQuerySyntaxException) is mapped to HTTP 500 with the fixed body Query execution failed.. Its cause, which may name SQL or tables, stays attached to the exception, so the call logger and ThrownStore record it while the client never sees it.