Logging and Debugging
The following methods provide logging of requests and responses:
The following example shows the results of logging all requests that end with /bean.
// A simple bean we're going to round-trip.
MyBean bean = new MyBean();
bean = RestClient
.create()
.json5()
.logRequests(DetailLevel.FULL, Level.SEVERE, (req,res)->req.getUri().endsWith("/bean"))
.logToConsole()
.build()
.post("http://localhost/bean", bean)
.run()
.getContent().as(MyBean.class);
This produces the following console output:
=== HTTP Call (outgoing) ======================================================
=== REQUEST ===
POST http://localhost/bean
---request headers---
Accept: application/json5
---request entity---
Content-Type: application/json5
---request content---
{f:1}
=== RESPONSE ===
HTTP/1.1 200
---response headers---
Content-Type: application/json
---response content---
{f:1}
=== END =======================================================================",
It should be noted that if you enable request logging detail level DetailLevel.FULL, response bodies will be cached by default which may introduce a performance penalty. Additionally, the following method is also provided for enabling debug mode:
Enabling debug mode has the following effects:
- RestClient.Builder.logToConsole() is called. Classic REST client logging/debug APIs are unchanged and remain available through the classic builder:
Next-generation REST client (NG)
The next-generation client (org.apache.juneau.rest.client.RestClient) uses JUL-level-driven debug logging through RichLogger.
Default logger name: RestClient.class.getName().
Debug detail is controlled solely by the resolved JUL logger level:
INFO: basic summary line.FINE: basic + request/response headers.FINEST: basic + headers + bounded request/response body capture — only if body dumping has been explicitly opted in via theJUNEAU_REST_DEBUG_ALLOW_DUMP_BODIESenvironment variable (see Secure by default below); otherwiseFINESTemits a suppression placeholder instead of body content.
Exactly one LogRecord is emitted per request/error, always at JUL INFO — the resolved tier controls message
detail only, so FINE/FINEST-selected detail still travels through INFO sinks. This applies uniformly to both
places the pipeline emits from: the normal request-completion path and the transport-exception (synthetic error
response) path.
Secure by default
BasicRestClientDebugFormatter mirrors the NG REST server's BasicRestDebugFormatter secret-hardening design (see
REST server Logging/Debugging » Secure by default):
raising a client's debug logger to FINE/FINEST can never, by itself, turn the log stream into a plaintext-credential
dump or a log-forging channel.
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 the same environment-variable master gate the server uses:
export JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES=true
Semantics are byte-identical to the server's gate: environment variable only (no system-property fallback), read
once per JVM and cached, trim-then-parse truthy (any non-empty trimmed value enables except case-insensitive
false/0). With the gate unset, formatBody emits a suppression placeholder naming the environment variable plus
the captured byte count and content-type — never body bytes in any representation (plaintext, hex, or otherwise),
even if a scrubber is configured. With the gate set, bodies render only when the content is renderable (see below);
non-renderable/binary content, or a non-identity Content-Encoding (e.g. gzip), yields a distinct "not rendered"
placeholder instead.
Setting JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES re-enables plaintext request/response body logging at FINEST for
both the client and the server. Treat FINEST client logs as sensitive, and supply a bodyScrubber(...) if any
secret-bearing bodies (tokens, PII, card data) are possible. Prefer leaving the gate unset in production.
isBodyRenderable(String contentType) (on the RestClientDebugFormatter SPI) decides whether a body is text worth
logging, using the same predicate as the server's RestDebugFormatter.isBodyRenderable: renderable for text/*,
application/json, application/xml, any +json/+xml suffix type, and application/x-www-form-urlencoded;
non-renderable for everything else, including multipart/form-data, absent, or blank content types. The decision is
made independently per side — the request's own Content-Type governs the request body, the response's own governs
the response body.
Body scrubbing SPI
When body dumping is opted in, transform body text before it's logged via the
RestDebugBodyScrubber
SPI, registered with bodyScrubber(...):
new BasicRestClientDebugFormatter().bodyScrubber((contentType, body) -> body.replaceAll("\"password\":\"[^\"]*\"", "\"password\":\"***\""));
- Gate-subordinate: the scrubber runs only after the master gate has already permitted dumping; it merely chooses scrubbed-vs-raw text and never causes a body to be emitted while the gate is unset.
- 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.
No default scrubber ships: with the gate set and no scrubber, the raw body is dumped (sanitized + capped).
Header redaction — widened set, separator-normalized
At the headers tier (FINE-or-finer), header values are masked with [REDACTED] 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; this
widened set is client-formatter-local, matching the server's own decision to keep its widened set formatter-local
rather than merged into the shared default. Configure the masked set with redactedHeaders(Collection<String>)
(replaces the masked set).
Log-forging sanitization
The request method and URI, header names and values, and body text (raw or scrubber-produced) are passed through a
sanitizer that escapes CR/LF and other control characters to visible, inert forms (\r, \n, \uXXXX). A remote
server therefore cannot inject a newline into a response header/body to forge a second physical log line. The order
applied to every emitted string is mask → escape → cap.
Bounded body output
Captured body bytes are bounded at capture time by bodyCap(int) (default 8 KB), then …[truncated N bytes] marks
what was omitted; a second character-level cap (bodyCap() * 6) is applied after sanitization so a scrubber that
returns an oversized string is still bounded. Unlike the server's formatter, the client does not add separate
header-count/header-scan/field-length/URI-length bounding knobs — the client's header source is already a finite,
bounded list rather than an adversarial unbounded input stream, so those extra knobs have no client-side analog.
Request correlation (X-Request-Id)
RestClient automatically participates in end-to-end request correlation. By default, every outgoing request carries
an X-Request-Id header so a call can be traced through the server logs (which honor that inbound id — see
REST server » Always-on request correlation):
- Auto-send / mint. If the request has no
X-Request-Idheader, the client mints a fresh time-orderedUuid7and sends it. A header the caller already set (globally on the builder or per-request) is honored, never overwritten. - Pre-call setter.
RestRequest.requestId(String)sets the id for a single call fluently;getRequestId()reads the id that was (or will be) sent. - Echo capture. The client captures the
X-Request-Idthe server echoes on the response, exposed viaRestResponse.getRequestId(). When correlating logs, the client uses the echoed id when present and falls back to the sent id otherwise (the effective id). - Opt-out.
RestClient.Builder.sendRequestId(false)disables auto-sending entirely; an explicitly caller-suppliedX-Request-Idheader still travels.
The effective id flows into debug logging: RestClientDebugPipeline stamps each emitted LogRecord with a structured
requestId field (via LogRecordContext.attachIfAbsent(...)), and BasicRestClientDebugFormatter prepends a
human-readable [requestId=<id>] marker to the rendered message — mirroring the server's statusLine prefix so a
single request reads identically on both sides.
try (var client = RestClient.builder().build();
var res = client.get("http://localhost/widgets/123").run()) {
// Server-honored, echoed back:
var id = res.getRequestId();
}
// Explicit per-call id (honored, not overwritten):
client.get("/widgets/123").requestId("order-9f2c").run();
// Opt out of auto-sending entirely:
var quiet = RestClient.builder().sendRequestId(false).build();
For tests, capture emitted records directly via RichLogger.captureEvents(...):
var logger = RichLogger.getLogger("my.client.debug");
logger.setLevel(Level.FINEST);
try (var capture = logger.captureEvents(Level.FINEST);
var client = RestClient.builder()
.debugLoggerName("my.client.debug")
.debugFormatter(new BasicRestClientDebugFormatter().bodyCap(8 * 1024))
.build();
var res = client.post("http://localhost/bean").bodyString("{\"x\":1}").run()) {
res.body().asString();
var record = capture.last();
// assert on record.getLevel() and record.getMessage()
}