I was working on integrating with a new version of a vendor’s API. They claimed that authentication and everything like that remained the same across API versions. However, upon updating my code to point to their new API, I hit all kinds of SSL issues.
The old hostname: api.vendor.com
The new host: next_api.vendor.com
The issue? Why it’s that underscore, of course! I was using Clojure for this project and the JVM was incredibly upset about this.
The Problem
Underscores in hostnames violate RFC 952 and RFC 1123. Most HTTP clients and languages quietly ignore this, but Java’s URI class enforces the spec strictly. When you construct a URI with an underscore in the hostname, getHost() returns null. Any HTTP client that calls getHost() — which is most of them — throws an error before the request ever leaves the JVM.
The Reflection Hack (Don’t Do This)
My first instinct was to bypass URI validation with reflection, accessing the private host field directly to force the value in after construction. This got past the initial validation, but it was ugly and fragile.
Even with that hack in place, the request still failed. Java’s SSL/TLS layer performs its own hostname verification when opening the HTTPS socket, and the non-compliant hostname triggered a second rejection there. So now I had two problems.
The Nuclear Option (Really Don’t Do This)
Java provides a system property to disable hostname verification entirely:
-Djdk.internal.httpclient.disableHostnameVerification=true
This must be set at JVM startup — the property is read at class initialization time, so setting it after the class loads has no effect. More importantly, it disables hostname verification for the entire JVM. If you’re running a system that talks to multiple APIs, you’ve just opened every single one of them to man-in-the-middle attacks. Hard pass.
The Right Fix: A Scoped Custom TrustManager
The proper solution is to create a custom SSLContext with a permissive TrustManager, then use that context only for the problematic API client. This pattern works with any JVM HTTP client that accepts a custom SSLContext — java.net.http.HttpClient, hato, clj-http, OkHttp, etc. Here’s what it looks like in Clojure:
(import '[javax.net.ssl X509ExtendedTrustManager SSLContext TrustManager]
'[java.security SecureRandom])
(def permissive-trust-manager
(proxy [X509ExtendedTrustManager] []
(checkClientTrusted
([_ _ _] nil) ; 3-arity (with SSLEngine)
([_ _] nil)) ; 2-arity
(checkServerTrusted
([_ _ _] nil)
([_ _] nil))
(getAcceptedIssuers [] nil)))
(defn create-scoped-ssl-context []
(let [trust-managers (into-array TrustManager [permissive-trust-manager])]
(doto (SSLContext/getInstance "TLS")
(.init nil trust-managers (SecureRandom.)))))
;; Use this SSLContext only for the problematic client
(defn build-vendor-http-client []
(build-http-client {:ssl-context (create-scoped-ssl-context)}))
Two gotchas that cost me time:
Use
X509ExtendedTrustManager, notX509TrustManager. On newer JDKs, the SSL engine expects the extended version. If you implement the baseX509TrustManager, your custom trust manager will be silently ignored and you’ll get the same error.Implement both the 2-arity and 3-arity overloads of
checkClientTrustedandcheckServerTrusted. The 3-arity versions accept anSSLEngineorSocketparameter and are called during the handshake. If you only implement one, the other will throw anUnsupportedOperationExceptionat runtime.
This approach keeps the permissive SSL handling scoped to a single HTTP client instance. Every other connection in the JVM continues to use the default certificate verification.
The Best Fix: Talk to the Vendor
After implementing the workaround, I raised the hostname issue with the vendor. A few months later, they migrated to a compliant hostname. At that point, I was able to rip out all the custom SSL handling and use a standard HTTP client.
The custom TrustManager was always meant to be a bridge, not a permanent solution. If you find yourself writing one, file a ticket with the vendor. Non-RFC-compliant hostnames cause problems in more places than just Java — they can break DNS caching, CDN routing, and certificate issuance. The vendor probably doesn’t even know it’s an issue. A scoped workaround buys you time; fixing the root cause is what lets you delete the workaround.