Release 10.0.0: REST client
This page is part of the Release 10.0.0 release notes. The overview page lists the major changes, the breaking changes and the API changes for the whole release.
juneau-rest-client / juneau-rest-common
juneau-rest-clientis generally available in 10.0.0 and is the recommended REST client. The "Beta — API subject to change" labels are removed from its Javadoc and docs, and from the transport modules and the HTTP types injuneau-rest-commonandjuneau-rest-mock.juneau-rest-client-classicstays supported and is not deprecated.RestRequest.timeout(Duration)is now honored by every transport (bug fix) — previously onlyJavaHttpTransportapplied the per-request timeout; the Apache HttpClient 4.5/5, OkHttp, Jetty and mock transports silently ignored it. Each now uses its native per-request mechanism (HttpClient 4.5RequestConfigsocket/connect timeouts, HttpClient 5 request-scoped response timeout, OkHttp per-call read timeout, Jetty response-header wait, and a simulated handler deadline inMockHttpTransport) and throws aTransportExceptionon expiry. Anulltimeout leaves existing behaviour unchanged; zero or negative durations are rejected withIllegalArgumentExceptionbyRestRequest.timeout(...)on every transport.- Apache HttpClient 5 transport: credentials on cross-origin redirects with a caller-supplied client — with a caller-supplied
CloseableHttpClient, the transport passes a per-request context that removesAuthorization,Cookie,Proxy-AuthorizationandX-API-Keyfrom a redirect hop to another origin (scheme, host or port) and from every later hop, whateverRedirectStrategythe client uses (includingLaxRedirectStrategy); the client's own redirect settings are unchanged. On a proxied route that is not tunnelled (a plainhttptarget through an HTTP proxy),Proxy-Authorizationis kept, because it authenticates to the proxy that every hop goes through. AnAuthorizationthe client'sCredentialsProvideradds for the other origin is removed too. Clients fromHttpClients.createMinimal()now work with the transport. - Apache HttpClient 5 transport: cross-origin redirects followed with credentials stripped on the client it creates (behaviour change) — when the transport creates its own HttpClient, a redirect to another origin on a request carrying
Authorization,Cookie,Proxy-AuthorizationorX-API-Keyis now followed with those headers removed from that hop and every later hop, as on the other transports, using the same per-request context. Previously HttpClient'sDefaultRedirectStrategyrefused the redirect and the3xxresponse was returned. Same-origin redirects keep the headers; the redirect cap, circular-redirect check,POST-to-GETrewrite and SSRF-guarded requests are unchanged.
Secure-by-default NG REST client debug hardening (RestClientDebugFormatter)
The next-generation RestClient's debug logging pipeline (RestClientDebugPipeline /
BasicRestClientDebugFormatter) now gets the same stable-emission and secret-hardening treatment the server side's
RestDebugFormatter got above — ported onto the client's parallel classes, reusing the same
juneau-rest-common primitives (DebugTextSanitizer, RestDebugBodyScrubber). This is an unreleased-10.0 hard
break in the client debug formatter's output — there is no compatibility shim.
- Stable-
INFOemission. Every debugLogRecord— on both the normal response-close path and the transport-exception synthetic-response path — is now always stampedLevel.INFO; the resolved tier (FINE/FINEST) continues to control message detail only, not the emitted record level. - No body dumps by default. Request/response bodies are never dumped unless an operator explicitly sets the
same
JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIESenvironment variable the server uses (byte-identical name and trim-then-parse truthy semantics — one env var now gates both sides). With the gate unset,FINESTemits a suppression placeholder + byte count instead of body content, replacing the previous unconditional UTF-8 + hex dump. Even opted in, bodies render only for renderable text content (via a duplicatedisBodyRenderable(String)predicate onRestClientDebugFormatter, resolved independently per request/response side) and only for an identityContent-Encoding; non-renderable/binary content and non-identity encodings yield a distinct "not rendered" placeholder. RestDebugBodyScrubberopt-in, registered via the newbodyScrubber(...)builder method onBasicRestClientDebugFormatter— gate-subordinate and fail-closed, same contract as the server side: a throw ornullresult falls closed to a placeholder, never the raw body.- Widened, formatter-local header redaction, matching the server's added set (
X-Auth-Token,X-Authorization,WWW-Authenticate,Referer,Location, atop the sharedRedactedHeaders.DEFAULT), matched with separator-normalized folding (X-Auth-Token/X_Auth_Token/XAuthTokenall match). - Log-forging sanitization. The request method/URI, header names/values, and any dumped body text are escaped
(CR/LF and other control characters →
\r/\n/\uXXXX), so a server cannot inject a newline into a response header/body to forge a second physical log line.
Out of scope for this port (client-specific rationale): the server's maxHeaders/maxHeaderScan/maxFieldLength/
maxUriLength bounding knobs have no client-side analog, since the client's header source is already a finite,
bounded list rather than an adversarial unbounded servlet input.
See the Logging and Debugging topic page for the full model.
Client-side request correlation (X-Request-Id)
RestClient now auto-participates in end-to-end request correlation, pairing with the server's always-on
requestId resolver above.
- Auto-send / mint. Every outgoing request carries an
X-Request-Idheader; if the caller hasn't set one, the client mints a time-orderedUuid7. A caller-supplied header (builder-global or per-request) is honored, never overwritten.RestClient.Builder.sendRequestId(false)opts out of auto-sending. - Pre-call setter + getters.
RestRequest.requestId(String)sets the id for one call;RestRequest.getRequestId()reads the sent id;RestResponse.getRequestId()exposes the id the server echoed back. - Effective-id logging.
RestClientDebugPipelinestamps each emittedLogRecordwith a structuredrequestIdfield (echoed id when present, else the sent id) viaLogRecordContext.attachIfAbsent(...), andBasicRestClientDebugFormatterprepends a[requestId=<id>]marker to the rendered message — matching the server'sstatusLineprefix so a request reads identically on both sides. - Shared constants. The
X-Request-Idheader name and max length live in a sharedRequestIdConstantsinjuneau-rest-common, consumed by both client and server.
See the Logging and Debugging topic page.
Next-generation remote-proxy declarative features
Juneau 10.0 significantly expands what the REST proxy annotation family (@Remote, @RemoteOp, and the verb annotations @RemoteGet / @RemotePost / @RemotePut / @RemoteDelete / @RemotePatch) can express declaratively. These additions originally landed on the next-generation proxy engine only (RestClient.remote(...) → RemoteClient); a later classic/next-gen parity pass brought the classic getRemote(...) engine up to the same level for every member below except the method/interface interceptors() policy attribute and the declarative @Multipart marker, which remain next-generation-only (see each bullet for the current per-member status).
- Constant part values —
@Remote(interface) and@RemoteOp/ verb annotations (method) gainheaders/queryData/formDatamembers that emit always-applied constant headers, query parameters, and form-data fields on every call, with no dummy parameter. Method-level constants take precedence over interface-level ones, caller-supplied values still compose, and all values resolve throughVarResolver.DEFAULT. Honored by both engines. (Constant path values are out of scope;@Remote(headerList=…)remains classic-only.) @Content(def=…)honored — the next-gen engine now applies both the parameter-level body default (when the body argument isnull) and a param-less, method-level constant body, matching the classic engine.- Per-part serializers —
@HttpPartMarshalling(serializer=…)is now wired for outgoing query / header / path / form-data parts, with precedence parameter › method › interface and a fallback to the defaultOpenApiSerializerwhen absent. (Serializer side only; the parser member is not consumed by the next-gen engine.) - Declarative call policy —
@Remoteand@RemoteOp/ verb annotations gaininterceptors(),timeout(),retries(),retryNonIdempotent(), andthrowOnError(). Interceptors apply as a union (builder → interface → method); timeout/retries/throwOnError scalars follow method › interface › builder-default precedence. Auto-retries are conservatively safe: triggered only by connection failures or429/5xx, with exponential backoff, and gated to idempotent verbs (POST/PATCH require opt-in), repeatable bodies, and buffered (non-streaming) return modes only.timeout()/retries()/retryNonIdempotent()/throwOnError()are honored by both engines;interceptors()remains next-generation-only — the classic engine explicitly rejects it (with a one-time warning) because the classic and next-generationRestCallInterceptorSPI types are nominally incompatible. - Dynamic URL & base override — a new
@Urlparameter annotation (org.apache.juneau.http.Url) supplies the whole request URL at call time (absolute replaces and bypasses the client root URL; relative resolves against it), plus a declarativebaseUrl()attribute on@Remote/@RemoteOp/ verb annotations that substitutes the authority+root while preserving the path and{var}templating. Precedence:@Url› methodbaseUrl› interfacebaseUrl› clientrootUrl.@Url/baseUrl()are honored by both engines — the classicgetRemote(...)engine resolves them the same way. - Declarative multipart — a new method-level
@Multipartmarker (org.apache.juneau.http.remote.Multipart) plus a@Partparameter annotation (org.apache.juneau.http.Part, withname/value/fileName/contentType) build amultipart/form-datarequest from annotated parameters. Text,byte[],File,InputStream,Reader,HttpBody, and bean part sources are accepted; file/stream/reader/bean parts stream rather than buffer. A method is either multipart or single-@Content(validated at proxy-build time); hand-assembling aMultipartBodyas a@Contentbody remains available as an escape hatch. Next-generation-only; the classic engine has no equivalent multipart-assembly support. - Per-method format selection —
contentType()andaccept()attributes on@Remote/@RemoteOp/ verb annotations drive marshaller selection (not just header values).contentTypeselects the matching request serializer and emits a single cleanContent-Type;acceptsets theAcceptheader and acts as a fallback parser (the responseContent-Typestays authoritative). A no-match media type falls back to the default marshaller but still sends the overridden label (vendor types). Precedence: method › interface › client default; the dedicated attribute beats a constant header. Honored by both engines. - Streaming efficiency — POJO and
Readerrequest bodies now stream straight to the wire instead of being buffered, andReader/InputStreamreturn types hand the caller a lazy stream over the live response whose connection is released on close.
See the expanded Declarative Remote-Proxy Features section of the Next-Generation REST Client topic page for full examples and precedence rules.
Transport robustness — stale pooled-connection retry
The next-generation REST client's HTTP transports now transparently retry a request exactly once when a pooled keep-alive connection is torn down by the server before any response bytes are received (an immediate EOF / connection reset) — a common source of intermittent failures against servers that idle-close persistent connections. The retry is gated on provable safety: an idempotent HTTP method (GET, HEAD, PUT, DELETE, OPTIONS, TRACE) with an absent or repeatable body (TransportRequest.isSafeToReplay()). A POST or any other non-idempotent request is never replayed, so a request cannot double-execute a side effect.
JettyHttpTransportandApacheHc45Transportgained this in an earlier 10.0.0 change;JavaHttpTransport(the default, JDKHttpClient-backed transport) now matches them. The JDK client self-retries only body-lessGETs on such a failure, so the transport-level retry closes the gap for the other idempotent methods (e.g.PUT/DELETE).- A shared predicate,
TransportException.isStaleConnectionFailure(Throwable), recognizes the failure signature across transports (EOFException/ClosedChannelException/ a connection-resetSocketException/ the JDK's"HTTP/1.1 header parser received no bytes"message).
juneau-rest-client
REST session option wire helpers
RestClient.Builder and RestRequest provide fluent helpers for serializer/parser session options using the same HTTP names as the server (RestSharedConstants): default and per-request headers (JSON5 strings or property maps via Json5.of), and default or per-request query parameters (UON strings or property maps via Uon.of). MockRestClient.Builder and MockRestRequest override these methods for fluent typing.
Dynamic Root URL Support
-
rootUrl(Supplier<String>)overload:RestClient.Buildernow accepts aSupplier<String>for the root URL, allowing it to be computed dynamically at request time rather than fixed at build time. -
RestClient.getRootUrl(): New method to inspect the currently resolved root URL from a builtRestClient.Three common patterns are supported:
Swappable reference — use an
AtomicReferencewhen you need to point the client at a different host at any time:var urlRef = new AtomicReference<>("https://host1");
var client = RestClient.create().rootUrl(urlRef::get).build();
// Switch to a different host at runtime
urlRef.set("https://host2");Refreshable cached value — use
Memoizer(viaUtils.memoizer()) when computing the URL is expensive (e.g. service discovery) and you want it cached until explicitly refreshed:var url = memoizer(() -> serviceDiscovery.findUrl("my-service"));
var client = RestClient.create().rootUrl(url).build();
// Force re-evaluation on the next request (e.g. after failover)
url.reset();Purely dynamic — use a plain lambda when the URL must be re-evaluated on every request:
var client = RestClient.create().rootUrl(() -> config.getRootUrl()).build();The supplier is called at each read site (request creation, remote proxy invocation, RRPC path building), so URL changes are always reflected on the next request. No build-time URL validation is performed for the
Supplier<String>overload; invalid values will surface as errors when requests are made.
juneau-rest-client bug fixes
-
Fixed stream-closed exception when calling
RestResponse.getContent().asString()after FULL request logging.RestResponse.close()previously calledEntityUtils.consumeQuietly(...)on the underlying HTTP entity before rendering FULL-detail log output. On real (non-mock) responses the consume step closes the network input stream, so the subsequent FULL-logging body read (and any later user call togetContent().asString()) failed withIOException: Stream closed, leavingResponseContent.bodyunset. FULL-detail logging now renders the body before consuming the entity (withconsumeQuietly(...)moved into afinallyso non-logged and exceptional paths still drain the entity). -
RemoteClientnow treatsjava.timetypes as scalar parts rather than beans.TemporalandTemporalAmountvalues (e.g.Instant,LocalDate,Duration) passed to@Remoteproxy methods are now rendered as scalar request parts (path, query, header, form-data) instead of being introspected as beans.
juneau-rest-client migration notes
- RestClient
rootUrl: TherootUrlfield is now stored as aSupplier<String>internally. Code that relies on reflection to access the privaterootUrlfield directly (unusual but possible) will now see aSupplier<String>instead of aString.