Check also MIGRATION.md for possible compatibility problems.
Removed method contentTypeOfBody from HttpRequestBuilder.
Added method addHttpClientCustomizer into HttpRequestBuilder
Removed inputStreamDeserializer from ResponseDeserializer. Reason: Response should be deserialized
by ResponseDeserializer
Renamed ignorableDeserializer of ResponseDeserializer to toStringDeserializer
Due to the fact that old implementation was designed only for one predefined URI and wasn't much flexible the 2.x.x
version don't have back compatibility with version 1.x.x. The 1.x.x versions will be supported in branch 1.x.x_support
.
The method getContentAsString of ResponseBodyReaderContext become deprecated and will be removed in further
versions.
The ResponseBodyReaderContext become generic.
The path method of WebTarget now adds path to existed path instead of replacing, for replace use setPath method.
Headers are available from ResponseHandler
-
Starting with 3.3.0 can be used immutable web target.
HttpRequest.immutableTarget(uri) -
Now request methods supported an auto-serializing object to body for content types
application/jsonandapplication/xml -
Added
ResponseHandler.recuiredGet()method.
Added methods ClientBuilder.enableCookieManagement, ClientBuilder.enableAutomaticRetries and
ClientBuilder.setConnectionTimeToLive.
Added methods ClientBuilder.addDefaultConnectionManagerBuilderCustomizer.
- Charset handling was clarified and split:
WebTarget#setCharset(Charset)now sets both the query-string charset and the request body charset.- Added
WebTarget#setQueryCharset(Charset)to control only the URI query-string percent-encoding charset. - Added
WebTarget#setBodyCharset(Charset)to control only the request body charset used by request body converters. - URI path segments are always percent-encoded as UTF-8 per RFC 3986; if you need non-UTF-8 path
encoding, percent-encode the path yourself before passing it to
target(...).
- Default charset for both query-string and body is
UTF-8. HttpRequestBuilder.setDefaultJsonMapper/setDefaultXmlMappernow take a defensive copy of the supplied mapper at the moment the setter is called; the caller's instance is never mutated by the library.addResponseDefaultDateDeserializationPattern/addRequestDefaultDateSerializationPatternnow compose correctly with a user-supplied mapper — previously the patterns were silently dropped.- Retry API refresh (
@Beta— breaking):- New
RetryAttempttype exposesresponse,method,uri,attemptNumber, anderrorto retry predicates. RetryContextmethods now take aRetryAttempt:mustBeRetried(RetryAttempt),getRetryDelay(RetryAttempt)(returnsDuration),beforeRetry(RetryAttempt, WebTarget).- Removed the pre-3.5.0 response-only overloads (
mustBeRetried(Response),getRetryDelay(Response),beforeRetry(WebTarget)). 3.4.x customRetryContextimplementations will no longer compile — seeMIGRATION.mdfor the before/after template. - The default
mustBeRetried(RetryAttempt)is idempotency-gated: only retries GET/HEAD/OPTIONS/PUT/DELETE/TRACE (RFC 9110 §9.2.2) on 503 by default. Retrying POST/PATCH must be an explicit opt-in — see the new factory helperRetryContext.onAnyMethod5xx(int, Duration). - New factory helper
RetryContext.onIdempotent5xx(int, Duration)— safe default that retries idempotent methods on any 5xx, honoringRetry-Afterwhen present. - Added
HttpMethod.isIdempotent()per RFC 9110.
- New
- Retries now work for requests with repeatable bodies. Previously, any retryable request whose
HttpEntityhad been initialized failed on first retry with "After initializing the httpEntity builder can't be copied."HttpUriRequestBuildernow shares the entity reference with the copy whenentity.isRepeatable()istrue(covers all built-in body paths:StringEntity,ByteArrayEntity,FileEntity, and Jackson-produced bodies). Non-repeatable entities (InputStreamEntityand similar streaming sources) still cannot be replayed and now fail with an actionable error pointing at the fix. Response#getContentType()no longer leaksUnsupportedCharsetException(or any otherIllegalArgumentExceptionfromContentType.parse) when a server returns a header likeContent-Type: text/plain; charset=<charset-not-installed-in-this-JVM>. The malformed value is logged at WARN level and the helper returnsnull— the same as "no Content-Type header." Reader chains (isReadable()predicates) now fall through cleanly toResponseBodyReaderNotFoundExceptioninstead of letting an unchecked exception escape from a getter.- Replaced the custom
LimitedInputStreamwithcommons-io'sorg.apache.commons.io.input.BoundedInputStream. The response-body size cap is now enforced by configuringBoundedInputStreamwithsetMaxCount(maxBytes + 1)(so a body of exactlymaxBytesstill drains cleanly to EOF) and ansetOnMaxCountconsumer that throwsInvalidContentLengthExceptionthe moment the cap is exceeded. The swap closes three latent gaps in the previous custom class in one stroke:skip()andread(b, off, len)no longer over-pull the underlying stream past the cap (commons-io clamps both viatoReadLen),markSupported()no longer falsely advertises mark support that would corrupt the byte counter onreset(), and we drop ~70 lines of custom code in favor of a battle-tested upstream implementation.commons-io 2.22.0is now a compile-scope dependency. StringReaderandByteReaderno longer passmaxLentoEntityUtils.toString/EntityUtils.toByteArray. The cap is enforced at the byte level by the wrappedBoundedInputStream; passing amaxLen(which is a character limit fortoString) would silently truncate the result at a char boundary instead of triggering the byte-cap throw.BoundedHttpEntity#writeTois now documented and regression-tested as a load-bearing override for the size-cap guarantee: without itHttpEntityWrapper#writeTowould delegate straight to the wrapped entity, bypassingBoundedInputStreamand silently spooling oversize bodies through callers that useresponse.getEntity().writeTo(...)(e.g. spooling a response to disk). The override now also usesIOUtils.copy(commons-io) instead of a hand-rolled buffered loop.ClientBuilderdefaultdefaultMaxPoolSizePerRoutelowered from128to32.maxPoolSizeis unchanged at128. PreviouslyperRoute == totallet a single hot host saturate the entire pool, leaving parallel requests to other hosts blocked onconnectionRequestTimeout. The newtotal / 4ratio matches industry conventions (Apache HC, AWS SDK, Spring) and preserves multi-host fairness. Single-upstream workloads that want the old behavior should callsetDefaultMaxPoolSizePerRoute(128)explicitly — seeMIGRATION.md.ClientBuilder.proxy(URI)now rejects URIs that include userinfo (e.g.http://user:pass@proxy.corp) withIllegalArgumentExceptionand a message pointing at Apache HC5'sBasicCredentialsProvider. Previously, the userinfo was silently discarded by theHttpHostconstructor — users who thought they were configuring proxy auth would see no credentials sent.ClientBuilder.build()is now idempotent across repeated calls. Previously, thedefaultRequestConfigBuilderanddefaultConnectionConfigBuilderwere stored as fields and user-supplied customizers were applied to those persistent instances on everybuild()— state-dependent customizers (e.g. ones that re-register an interceptor or react to current builder state) would compound across calls. Eachbuild()now snapshots the configured state and applies customizers to a freshBuilderper call.- New
setDefaultResponseCharset(Charset)onHttpRequestBuilder. The library now defaults to UTF-8 for response bodies whoseContent-Typelacks acharsetparameter (Apache HC5's bare default is ISO-8859-1). Server-suppliedcharset=...always wins; the new setting only affects the no-charset path. SeeMIGRATION.mdfor restoring the ISO-8859-1 behavior on legacy servers. - New opt-in SSRF guard
ClientBuilder.disallowPrivateAndLoopbackHosts(). When enabled, the client's DNS resolver rejects any host that resolves to loopback / unspecified / link-local / RFC 1918 / IPv6 unique-local addresses. The check is plumbed through Apache HC5'sDnsResolver, so it fires for every host the client touches — including hosts reached via 3xx redirects, not only the URL the caller passed totarget(...). The same DNS lookup that produces the connection IP is the one being filtered, closing the time-of-check / time-of-use gap a URL-only check would have. Specifically catches user-controlled URLs pointing at cloud-metadata endpoints (e.g.169.254.169.254). Combine with network-layer egress filtering for full defence-in-depth. - New TLS knobs on
ClientBuilder:setTlsVersions(String...)enforces a TLS version allow-list (e.g."TLSv1.3", "TLSv1.2");setCipherSuites(String...)opts out of weak / deprecated ciphers. Both delegate to Apache HC5'sClientTlsStrategyBuilderand use JVM defaults when not called. - New HTTP/1.1 head-size knobs on
ClientBuilder:setMaxHeaderCount(int)caps the per-message header count;setMaxLineLength(int)caps any single line (status line or header). Bounds memory consumption when talking to a hostile / buggy server that emits an unbounded header list — complementssetMaxResponseBodySizeBytes(which only protects the body, not the head). Negative values mean "use Apache HC5's built-in default." - New
HttpRequestBuilder.addPayloadRedactor(Function<String, String>)for masking secrets in request bodies before they are logged viaenableRequestPayloadLogging(). Redactors compose left-to-right; a buggy redactor that throws is caught and swapped for a[redaction-failed]placeholder rather than killing the in-flight request. Redactors are only invoked when payload logging is enabled. - Successful
HEADresponses are no longer remapped to 502. Apache HC5 returnsnullHttpEntityforHEADresponses (HTTP forbids a response body onHEAD), and the previous code blindly treatedhasBody(statusCode) && entity == nullas a server error. The check now excludesHEAD; for non-HEADrequests the existing safety net is kept (Apache HC5 always sets a, possibly length-0, entity forhasBody(...)statuses on those methods, so this branch effectively only fires on genuinely malformed responses). - New
RetryContext.withMaxHonoredRetryAfter(RetryContext, Duration)opt-in clamp onRetry-After. Wraps anyRetryContextand caps the honoured retry delay, defending against a misbehaving or hostile upstream returningRetry-After: 99999(~28 hours) and stalling the calling thread. Default behaviour without the wrapper is unchanged — the library is RFC-compliant and honours whatever the server says. - New
ClientBuilder.disallowPrivateAndLoopbackHosts(Predicate<InetAddress>)overload for the SSRF guard. The predicate is an allow-list escape hatch: it is invoked for each resolved address that would otherwise be blocked, and a return oftruepermits the address through. Useful when you need to block public-internet SSRF surface but still talk to a specific internal endpoint (e.g. an internal config service on10.0.7.42). Passingnullfor the predicate is equivalent to the no-arg overload. - New
HttpRequestBuilder.setDefaultQueryCharset(Charset)andHttpRequestBuilder.setDefaultBodyCharset(Charset). Set the initial value that every {@code WebTarget} produced from the resulting {@code HttpRequest} starts with — applied to targets returned by {@code target(...)}, {@code immutableTarget(...)}, and {@code retryableTarget(...)}. Per-target {@code setQueryCharset} / {@code setBodyCharset} still overrides; passing {@code null} at builder level keeps the library default (UTF-8). Closes the gap where consumers wanting a non-UTF-8 default had to override on every per-call target. {@code setCharset(Charset)} combo on the builder is intentionally not added — explicit per-axis is the only entry point at the builder level, mirroring the pre-existing {@code setDefaultResponseCharset} pattern.
- Upgraded Apache HttpClient 5 (
httpclient5) to5.6.1. Notable upstream behaviour change: the defaultHostnameVerificationPolicyis nowBUILTIN— aHostnameVerifierset viaClientBuilder.hostnameVerifier(...)alone is silently ignored. Pair the call withhostnameVerificationPolicy(HostnameVerificationPolicy.CLIENT)to keep the verifier authoritative (or usetrustAllHosts(), which already wires both). See the Javadoc onClientBuilder.hostnameVerifier(...)for the full note. No other API or behaviour changes.
- Java baseline raised to Java 17. Bytecode target moves from Java 8 to Java 17;
the build enforcer now rejects JDKs below 17. The
3.6.xline remains the supported Java 8 baseline. No public API was renamed, removed, or re-typed — source built against3.6.xrecompiles unchanged on4.0.0. SeeMIGRATION.md. - Published as a JPMS module
com.jsunsoft.http. Addssrc/main/java/module-info.javadeclaring the module withexports com.jsunsoft.httpandexports com.jsunsoft.http.annotations. Apache HttpClient 5 (client + core) and Jacksondatabind/dataformat-xmlarerequires transitive, so modulepath consumers see the public-API types from those modules without re-declaring them. Classpath consumers are unaffected. - Internal modernization to Java 9–17 idioms (no behaviour change): defensive collection
copies now use
List.copyOf/Set.copyOf;HttpMethod.isIdempotent()is a switch expression with compile-time-exhaustive enum cases; pattern-matchinginstanceofapplied inBasicResponseHandler,BasicWebTarget, andTypeReference. - Build:
maven-compiler-pluginconfiguration collapsed to a single<release>17</release>(previously a dual main/test execution); added an explicitmaven-surefire-pluginwith<useModulePath>false</useModulePath>so tests run on the classpath without forcing the production module toopensits package to reflective frameworks.
-
Jackson 2.x → 3.x.
jackson-databindandjackson-dataformat-xmlmove fromcom.fasterxml.jackson.{core,dataformat}totools.jackson.{core,dataformat}(3.1.3); JPMS module names follow (tools.jackson.databind,tools.jackson.core,tools.jackson.dataformat.xml).jackson-annotationsstays oncom.fasterxml.jackson.core:jackson-annotations(2.21) — the annotations artifact is intentionally shared between Jackson 2.x and 3.x consumers.jackson-datatype-jdk8,jackson-datatype-jsr310, andjackson-module-parameter-namesare folded intojackson-databindin 3.0 and their standalone declarations are dropped. The Jackson 3 BOM is imported independencyManagementso alltools.jackson.*coordinates resolve to a single coherent release train. See MIGRATION.md for the full upgrade guide — built-in modules, immutable-mapper consequences, exception-hierarchy changes, default-toggle flips, and theHEURISTICsingle-arg constructor binding behaviour. -
Mapper handling refactored for Jackson 3's immutable model.
HttpRequestBuilder'ssetDefaultJsonMapper(ObjectMapper)andsetDefaultXmlMapper(ObjectMapper)no longer take a defensive.copy()— Jackson 3 mappers are immutable, so the supplied reference is stored directly; per-config date-pattern overrides applied viaaddResponseDefaultDateDeserializationPattern/addRequestDefaultDateSerializationPatternare installed on a fresh derivative produced viaObjectMapper#rebuild(), leaving the caller's mapper untouched. -
Parse-failure routing tightened. Malformed-JSON / type-mismatch responses now surface as
ResponseBodyReaderException(→ HTTP 502 Bad Gateway on theResponseHandler), consistent withInvalidContentLengthException. In 4.0.0 these incidentally surfaced as 503 Service Unavailable because Jackson 2.x'sJsonProcessingExceptionextendedIOException; Jackson 3 fixed the hierarchy and the library follows the corrected semantics. Stream-level IO failures still route to 503; size-cap failures still route to 502. -
Jackson 3 default-toggle changes are accepted as-is rather than papered over with
builderWithJackson2Defaults()— see MIGRATION.md for the per-toggle table (most are strict/safe wins; the visible deltas areREAD/WRITE_ENUMS_USING_TO_STRINGandSORT_PROPERTIES_ALPHABETICALLY). -
Default date-format patterns removed (breaking). The default mappers no longer install the implicit
HH:mm:ss/dd/MM/yyyy/dd/MM/yyyy HH:mm:sspatterns forLocalTime/LocalDate/LocalDateTime— Jackson 3's standard ISO-8601java.timehandling applies instead. Re-register the legacy patterns viaaddResponseDefaultDateDeserializationPattern(...)/addRequestDefaultDateSerializationPattern(...)if your upstream still emits them — see MIGRATION.md. The now-unusedDateDeserializeContext/BasicDateDeserializeContext/DefaultDateDeserializeContexttypes are@Deprecated(forRemoval = true). -
Bounded close-drain for size-capped responses (fixes an unbounded cap-bypassing read). With
setMaxResponseBodySizeBytesconfigured,Response.close()previously ended up reading the entire unread remainder (Apache HC5's ownresponse.close()self-exhausts the raw entity stream, bypassing the cap) — a hostile oversized body was read to the end on close.close()now drains through the cap-enforcing stream under a dual bound — at most a configurable byte allowance within a hard wall-clock timeout (the drain runs on a shared daemon thread, JDKKeepAliveStreamCleaner-style) — and otherwise detaches the entity and discards the connection instead of reading further. New@Betaknobs onHttpRequestBuilder:setMaxResponseDrainSizeBytes(long)(default65536;0= never drain; negative = unlimited, bounded only by the cap and the timeout) andsetResponseDrainTimeout(Duration)(default 100 ms; must be positive). Both require the response body size cap and are rejected atbuild()without it. Uncapped behavior is unchanged (full consume on close). -
ClientBuilder.setMaxPoolSizePerRoutenow actually caps the routes it names. The cap was registered as a barenew HttpRoute(host), which never matched the runtime route shape the pool is keyed by: a portlessHttpHostfailedbuild()with Apache's "Target port must not be negative", and an https host's cap was silently ignored on every HC5 version (the registered route lacked the secure flag), as was any cap when a proxy was configured (missing proxy hop). Registration now mirrors the runtime route planner: scheme-default port resolved, secure flag derived from the scheme, configured proxy included. Caps underuseDefaultProxy()or a custom route planner remain build-time-unknowable and are documented as such. -
Pool-size setters validate their input (breaking for misconfigurations).
ClientBuilder.setMaxPoolSize/setDefaultMaxPoolSizePerRoute/setMaxPoolSizePerRoutenow throwIllegalArgumentExceptionfor values< 1(andNullPointerExceptionfor a null host). Previously such values were accepted and then silently ignored by Apache HC5's pool builder, which fell back to its internal defaults (25 total / 5 per route) — a configuredsetMaxPoolSize(0)quietly produced a 25-connection pool. -
No more spurious WARN for body-less non-success responses. A typed request receiving an entity-less non-success response (e.g. a 304 with redirect handling disabled — the default) no longer attempts to read error text: readers are not consulted for a body that isn't there and
getErrorText()isnull. The outcome is still traced — the "Unexpected Response …" log line fires for every non-success response (WARN for 400, DEBUG otherwise), with or without a body. The related "Can't find body reader … when http entity is null" log inreadEntityis demoted from WARN to DEBUG — returningnullfor an entity-less response is the documented contract, not an anomaly. -
Verbose logging no longer leaks credentials or payloads (security). With TRACE enabled on
com.jsunsoft.http, several log points printed secrets verbatim, bypassing the configured payload redactor. All are closed:HttpUriRequestBuilder.toString()(the "Executing request: …" TRACE) masksAuthorization,Proxy-Authorization,CookieandSet-Cookieheader values as<redacted>and prints the entity's class instead of its content;ResponseHandler.toString()(the "Executed result: …" TRACE) reports the content's presence/type and a header count instead of dumping the body and every header value; the request-serialization and response-result traces log types, not values; and the response reader's whole-body "Content is: …" TRACE is removed. Non-success error text (thegetErrorText()diagnostic channel) and request URIs/query parameters are still logged — don't put secrets in query strings. -
enableRequestPayloadLoggingnow coversObjectpayloads. Thepost(Object)/rawRequest(method, Object)family — the main JSON path — previously never produced the redacted payload log; only theStringoverloads did. The serialized wire form is now routed through the composed redactors, exactly likeStringpayloads. A repeatable serialized entity is read back for logging (briefly holding a second in-memory copy); a non-repeatable one is never consumed — a placeholder is logged instead. RawHttpEntitybodies remain excluded from payload logging (also when passed through theObjectoverloads): an entity may be arbitrarily large or binary, and reading it back for a log line could materialize it on the heap. -
SSRF guard covers the full IANA non-global address space (security).
disallowPrivateAndLoopbackHosts()previously rejected only loopback / unspecified / link-local / RFC 1918 / IPv6 unique-local addresses — a request to e.g.100.64.0.1(carrier-grade NAT space) sailed past the guard and attempted a real connection. The resolver now also blocks0.0.0.0/8, CGNAT100.64.0.0/10,192.0.0.0/24, the TEST-NETs (192.0.2.0/24,198.51.100.0/24,203.0.113.0/24), benchmarking198.18.0.0/15, reserved240.0.0.0/4(incl. broadcast), multicast, the NAT64 local-use prefix64:ff9b:1::/48, and IPv6 forms embedding an IPv4 address (NAT6464:ff9b::/96, IPv4-mapped/-compatible) when the embedded IPv4 is itself non-global —64:ff9b::<public v4>still resolves, so legitimate NAT64 clients keep working. The allow-list predicate overload applies to the new ranges too. The javadoc now documents the guard's limitations: asetDnsResolver(...)call inside a connection-manager customizer replaces the guard, and with a proxy configured the proxy — not the guarded resolver — resolves the target host. -
basicAuth(String, char[])zeroes the password on every path (security). The zeroingfinallywrapped only the final header registration: a username rejected for containing':'(or any encoding failure) returned with the caller's password array — and every intermediate secret buffer — intact. The whole body now runs intry/finally; the array and all intermediate copies are zeroed whether the method completes normally or throws. Only anullpassword (NPE, nothing to zero) is exempt. Callers must not reuse the array after the call, even a failed one — which the documented contract ("zeroed before this method returns") always implied. -
The SSRF guard can no longer be silently replaced by a customizer (breaking).
disallowPrivateAndLoopbackHosts()used to install its guarded DNS resolver before the connection-manager customizers ran, so asetDnsResolver(...)insideaddDefaultConnectionManagerBuilderCustomizer(...)silently disabled the guard. The guard is now installed after customizers and takes precedence: enabling it means resolution goes through the guarded system-default resolver, full stop. Anyone who relied on a customizer-supplied resolver alongside the guard was running without SSRF protection without knowing it — if specific internal endpoints must stay reachable, use thedisallowPrivateAndLoopbackHosts(Predicate)allow-list overload; a custom resolver combined with the guard is not supported. -
getDuration()is now measured with the monotonic clock. Request duration was computed fromSystem.currentTimeMillis(), so an NTP wall-clock step mid-request skewed it (even negative). The entire timing path — request start stamp,ResponseHandler.getDuration(), and the elapsed-time debug logs — now usesSystem.nanoTime(). -
Dead and unfulfillable API removed (breaking). Removed the never-thrown
InvalidMimeTypeExceptionand the three deprecatedResponseBodyReaderstatics (stringReader(),whenNonSuccessStringReader(),whenSuccessStringReader()) together with the two readers only they exposed.UnexpectedResponseExceptionis no longer@Deprecated: it is the superclass of its two designated replacements (UnexpectedStatusCodeException,MissingResponseBodyException), so the advertised removal could never happen — it is now documented as the permanent base type; catch it to handle both. -
Documentation now matches behavior.
getErrorText()javadoc describes what is actually returned (non-success body, failure exception messages, or both;nullwhen none) instead of a stale "connection failed but server sent useful data" description with a bogus@throws UnsupportedOperationException;ifHasContentdocumentsNullPointerExceptionfor a null consumer (whatArgsCheck.notNullactually throws);removeHeader's null-validation message names the right parameter;WebTargetjavadoc typos fixed. The "Basic authentication is being used" log is demoted from WARN to INFO — it fired unconditionally even for HTTPS-only clients.