Skip to main content

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-client is 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 in juneau-rest-common and juneau-rest-mock. juneau-rest-client-classic stays supported and is not deprecated.
  • RestRequest.timeout(Duration) is now honored by every transport (bug fix) — previously only JavaHttpTransport applied 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.5 RequestConfig socket/connect timeouts, HttpClient 5 request-scoped response timeout, OkHttp per-call read timeout, Jetty response-header wait, and a simulated handler deadline in MockHttpTransport) and throws a TransportException on expiry. A null timeout leaves existing behaviour unchanged; zero or negative durations are rejected with IllegalArgumentException by RestRequest.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 removes Authorization, Cookie, Proxy-Authorization and X-API-Key from a redirect hop to another origin (scheme, host or port) and from every later hop, whatever RedirectStrategy the client uses (including LaxRedirectStrategy); the client's own redirect settings are unchanged. On a proxied route that is not tunnelled (a plain http target through an HTTP proxy), Proxy-Authorization is kept, because it authenticates to the proxy that every hop goes through. An Authorization the client's CredentialsProvider adds for the other origin is removed too. Clients from HttpClients.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-Authorization or X-API-Key is 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's DefaultRedirectStrategy refused the redirect and the 3xx response was returned. Same-origin redirects keep the headers; the redirect cap, circular-redirect check, POST-to-GET rewrite 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-INFO emission. Every debug LogRecord — on both the normal response-close path and the transport-exception synthetic-response path — is now always stamped Level.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_BODIES environment variable the server uses (byte-identical name and trim-then-parse truthy semantics — one env var now gates both sides). With the gate unset, FINEST emits 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 duplicated isBodyRenderable(String) predicate on RestClientDebugFormatter, resolved independently per request/response side) and only for an identity Content-Encoding; non-renderable/binary content and non-identity encodings yield a distinct "not rendered" placeholder.
  • RestDebugBodyScrubber opt-in, registered via the new bodyScrubber(...) builder method on BasicRestClientDebugFormatter — gate-subordinate and fail-closed, same contract as the server side: a throw or null result 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 shared RedactedHeaders.DEFAULT), matched with separator-normalized folding (X-Auth-Token/X_Auth_Token/XAuthToken all 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-Id header; if the caller hasn't set one, the client mints a time-ordered Uuid7. 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. RestClientDebugPipeline stamps each emitted LogRecord with a structured requestId field (echoed id when present, else the sent id) via LogRecordContext.attachIfAbsent(...), and BasicRestClientDebugFormatter prepends a [requestId=<id>] marker to the rendered message — matching the server's statusLine prefix so a request reads identically on both sides.
  • Shared constants. The X-Request-Id header name and max length live in a shared RequestIdConstants in juneau-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) gain headers / queryData / formData members 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 through VarResolver.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 is null) 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 default OpenApiSerializer when absent. (Serializer side only; the parser member is not consumed by the next-gen engine.)
  • Declarative call policy — @Remote and @RemoteOp / verb annotations gain interceptors(), timeout(), retries(), retryNonIdempotent(), and throwOnError(). 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 or 429/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-generation RestCallInterceptor SPI types are nominally incompatible.
  • Dynamic URL & base override — a new @Url parameter 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 declarative baseUrl() attribute on @Remote / @RemoteOp / verb annotations that substitutes the authority+root while preserving the path and {var} templating. Precedence: @Url › method baseUrl › interface baseUrl › client rootUrl. @Url / baseUrl() are honored by both engines — the classic getRemote(...) engine resolves them the same way.
  • Declarative multipart — a new method-level @Multipart marker (org.apache.juneau.http.remote.Multipart) plus a @Part parameter annotation (org.apache.juneau.http.Part, with name/value/fileName/contentType) build a multipart/form-data request 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 a MultipartBody as a @Content body remains available as an escape hatch. Next-generation-only; the classic engine has no equivalent multipart-assembly support.
  • Per-method format selection — contentType() and accept() attributes on @Remote / @RemoteOp / verb annotations drive marshaller selection (not just header values). contentType selects the matching request serializer and emits a single clean Content-Type; accept sets the Accept header and acts as a fallback parser (the response Content-Type stays 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 Reader request bodies now stream straight to the wire instead of being buffered, and Reader / InputStream return 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.

  • JettyHttpTransport and ApacheHc45Transport gained this in an earlier 10.0.0 change; JavaHttpTransport (the default, JDK HttpClient-backed transport) now matches them. The JDK client self-retries only body-less GETs 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-reset SocketException / 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.Builder now accepts a Supplier<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 built RestClient.

    Three common patterns are supported:

    Swappable reference — use an AtomicReference when 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 (via Utils.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 called EntityUtils.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 to getContent().asString()) failed with IOException: Stream closed, leaving ResponseContent.body unset. FULL-detail logging now renders the body before consuming the entity (with consumeQuietly(...) moved into a finally so non-logged and exceptional paths still drain the entity).

  • RemoteClient now treats java.time types as scalar parts rather than beans. Temporal and TemporalAmount values (e.g. Instant, LocalDate, Duration) passed to @Remote proxy 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: The rootUrl field is now stored as a Supplier<String> internally. Code that relies on reflection to access the private rootUrl field directly (unusual but possible) will now see a Supplier<String> instead of a String.