Bug description
Authentication to Azure OpenAI performs an SSL connection to www.example.com, that fails in some cases (e.g. if a custom certificate is required).
Environment
Fails with Spring AI 2.0.0 but worked fine with Spring AI 1.x.
Steps to reproduce
We use Spring AI with LLMs from Azure OpenAI.
The Azure OpenAI authentication does not use API key but the Azure CLI (for local dev) or managed identities (when hosted on Azure).
With Spring AI 1.x it worked fine. But with Spring AI 2.0.0 it now fails because of an SSL handshake failure.
It seems to come from a connection to https://www.example.com performed by the azure-identity library:
// Method from com.azure.identity.AuthenticationUtil of azure-identity 1.18.3 library from Spring Cloud Azure 7.3.0
public static Supplier<String> getBearerTokenSupplier(TokenCredential credential, String... scopes) {
HttpPipeline pipeline
= new HttpPipelineBuilder().policies(new BearerTokenAuthenticationPolicy(credential, scopes)).build();
return () -> {
// This request will never need to go anywhere; it is simply to cause the policy to interact with
// the user's credential
HttpRequest req = new HttpRequest(HttpMethod.GET, "https://www.example.com");
try (HttpResponse res = pipeline.sendSync(req, Context.NONE)) {
return res.getRequest().getHeaders().get(HttpHeaderName.AUTHORIZATION).getValue().split(" ")[1];
}
};
}
That code is called from org.springframework.ai.openai.setup.AzureInternalOpenAiHelper#getAzureCredential (line 40) called by org.springframework.ai.openai.setup.OpenAiSetup#azureAuthentication (line 328).
This fails in our company because some specific certificates are required to perform HTTPS connections.
The LangChain4j counterpart works fine (no SSL error):
- Same Spring Boot 4.1.0 version
- Same azure-identity from Spring Cloud Azure 7.3.0
- Same authentication mode (Azure CLI during local dev)
- But with LangChain4j 1.17.0 instead of Spring AI 2.0.0
We did not have to import the certificates for Spring AI 1.x. We also do not have to import the certificates for LangChain4j.
Moreover, connecting to www.example.com seems quite weird.
Expected behavior
Azure OpenAI authentication should work fine, and no SSL connection to www.example.com should be attempted.
If we add the certificates to the JVM, then it works fine. But doing so for multiple applications is a problem.
Considering that (a) it worked fine with Spring AI 1.x and (b) it works fine with LangChain4j, this seems like a Spring AI bug or regression.
What do you think ? Could this be fixed ?
Sample stack trace
java.io.UncheckedIOException: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem
at com.azure.core.http.netty.NettyAsyncHttpClient.sendSync(NettyAsyncHttpClient.java:200) ~[azure-core-http-netty-1.16.4.jar:1.16.4]
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:51) ~[azure-core-1.58.0.jar:1.58.0]
at com.azure.core.http.policy.BearerTokenAuthenticationPolicy.processSync(BearerTokenAuthenticationPolicy.java:175) ~[azure-core-1.58.0.jar:1.58.0]
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53) ~[azure-core-1.58.0.jar:1.58.0]
at com.azure.core.http.HttpPipeline.sendSync(HttpPipeline.java:138) ~[azure-core-1.58.0.jar:1.58.0]
at com.azure.identity.AuthenticationUtil.lambda$getBearerTokenSupplier$0(AuthenticationUtil.java:56) ~[azure-identity-1.18.3.jar:1.18.3]
at com.openai.credential.BearerTokenCredential.token(BearerTokenCredential.kt:35) ~[openai-java-core-4.39.1.jar:4.39.1]
at com.openai.core.ClientOptions.securityHeaders$openai_java_core(ClientOptions.kt:743) ~[openai-java-core-4.39.1.jar:4.39.1]
at com.openai.core.PrepareRequest.prepare(PrepareRequest.kt:25) ~[openai-java-core-4.39.1.jar:4.39.1]
at com.openai.services.blocking.chat.ChatCompletionServiceImpl$WithRawResponseImpl.create(ChatCompletionServiceImpl.kt:134) ~[openai-java-core-4.39.1.jar:4.39.1]
at com.openai.services.blocking.chat.ChatCompletionServiceImpl.create(ChatCompletionServiceImpl.kt:63) ~[openai-java-core-4.39.1.jar:4.39.1]
at com.openai.services.blocking.chat.ChatCompletionService.create(ChatCompletionService.kt:64) ~[openai-java-core-4.39.1.jar:4.39.1]
at org.springframework.ai.openai.OpenAiChatModel.lambda$internalCall$2(OpenAiChatModel.java:217) ~[spring-ai-openai-2.0.0.jar:2.0.0]
at io.micrometer.observation.Observation.observe(Observation.java:634) ~[micrometer-observation-1.17.0.jar:1.17.0]
at org.springframework.ai.openai.OpenAiChatModel.internalCall(OpenAiChatModel.java:215) ~[spring-ai-openai-2.0.0.jar:2.0.0]
at org.springframework.ai.openai.OpenAiChatModel.call(OpenAiChatModel.java:194) ~[spring-ai-openai-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.advisor.ChatModelCallAdvisor.adviseCall(ChatModelCallAdvisor.java:58) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextCall$1(DefaultAroundAdvisorChain.java:117) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at io.micrometer.observation.Observation.observe(Observation.java:634) ~[micrometer-observation-1.17.0.jar:1.17.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextCall(DefaultAroundAdvisorChain.java:116) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor.adviseCall(SimpleLoggerAdvisor.java:76) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextCall$1(DefaultAroundAdvisorChain.java:117) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at io.micrometer.observation.Observation.observe(Observation.java:634) ~[micrometer-observation-1.17.0.jar:1.17.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextCall(DefaultAroundAdvisorChain.java:116) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.advisor.ToolCallingAdvisor.adviseCall(ToolCallingAdvisor.java:150) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextCall$1(DefaultAroundAdvisorChain.java:117) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at io.micrometer.observation.Observation.observe(Observation.java:634) ~[micrometer-observation-1.17.0.jar:1.17.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextCall(DefaultAroundAdvisorChain.java:116) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.lambda$doGetObservableChatClientResponse$1(DefaultChatClient.java:658) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at io.micrometer.observation.Observation.observe(Observation.java:634) ~[micrometer-observation-1.17.0.jar:1.17.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetObservableChatClientResponse(DefaultChatClient.java:656) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetObservableChatClientResponse(DefaultChatClient.java:636) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.content(DefaultChatClient.java:631) ~[spring-ai-client-chat-2.0.0.jar:2.0.0]
at com.redacted.service.DocumentService.summarize(DocumentService.java:32) ~[classes/:na]
at com.redacted.SpringAiDocumentApplication.summarize(SpringAiDocumentApplication.java:39) ~[classes/:na]
at com.redacted.SpringAiDocumentApplication.run(SpringAiDocumentApplication.java:30) ~[classes/:na]
at org.springframework.boot.SpringApplication.lambda$callRunner$1(SpringApplication.java:792) ~[spring-boot-4.1.0.jar:4.1.0]
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:82) ~[spring-core-7.0.8.jar:7.0.8]
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60) ~[spring-core-7.0.8.jar:7.0.8]
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:86) ~[spring-core-7.0.8.jar:7.0.8]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:800) ~[spring-boot-4.1.0.jar:4.1.0]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:791) ~[spring-boot-4.1.0.jar:4.1.0]
at org.springframework.boot.SpringApplication.lambda$callRunners$0(SpringApplication.java:776) ~[spring-boot-4.1.0.jar:4.1.0]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[na:na]
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[na:na]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:776) ~[spring-boot-4.1.0.jar:4.1.0]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:328) ~[spring-boot-4.1.0.jar:4.1.0]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1365) ~[spring-boot-4.1.0.jar:4.1.0]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-4.1.0.jar:4.1.0]
at com.redacted.SpringAiDocumentApplication.main(SpringAiDocumentApplication.java:19) ~[classes/:na]
Caused by: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem
at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.handshakeException(ReferenceCountedOpenSslEngine.java:1937) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.wrap(ReferenceCountedOpenSslEngine.java:877) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at java.base/javax.net.ssl.SSLEngine.wrap(SSLEngine.java:564) ~[na:na]
at io.netty.handler.ssl.SslHandler.wrap(SslHandler.java:1150) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.SslHandler.wrapNonAppData(SslHandler.java:1003) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1562) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.SslHandler.decodeNonJdkCompatible(SslHandler.java:1397) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1437) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:545) ~[netty-codec-base-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:484) ~[netty-codec-base-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296) ~[netty-codec-base-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:176) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.nio.NioIoHandler.processSelectedKeysOptimized(NioIoHandler.java:571) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:512) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196) ~[netty-transport-4.2.15.Final.jar:4.2.15.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1195) ~[netty-common-4.2.15.Final.jar:4.2.15.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.2.15.Final.jar:4.2.15.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.2.15.Final.jar:4.2.15.Final]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:388) ~[na:na]
at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:271) ~[na:na]
at java.base/sun.security.validator.Validator.validate(Validator.java:256) ~[na:na]
at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:284) ~[na:na]
at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:144) ~[na:na]
at io.netty.handler.ssl.EnhancingX509ExtendedTrustManager.checkServerTrusted(EnhancingX509ExtendedTrustManager.java:81) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.ReferenceCountedOpenSslClientContext$ExtendedTrustManagerVerifyCallback.verify(ReferenceCountedOpenSslClientContext.java:245) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.ReferenceCountedOpenSslContext$AbstractCertificateVerifier.verify(ReferenceCountedOpenSslContext.java:898) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.internal.tcnative.CertificateVerifierTask.runTask(CertificateVerifierTask.java:36) ~[netty-tcnative-classes-2.0.77.Final.jar:2.0.77.Final]
at io.netty.internal.tcnative.SSLTask.run(SSLTask.java:48) ~[netty-tcnative-classes-2.0.77.Final.jar:2.0.77.Final]
at io.netty.internal.tcnative.SSLTask.run(SSLTask.java:42) ~[netty-tcnative-classes-2.0.77.Final.jar:2.0.77.Final]
at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.runAndResetNeedTask(ReferenceCountedOpenSslEngine.java:1555) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.lambda$getDelegatedTask$0(ReferenceCountedOpenSslEngine.java:1583) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.SslHandler.runDelegatedTasks(SslHandler.java:1692) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1548) ~[netty-handler-4.2.15.Final.jar:4.2.15.Final]
... 21 common frames omitted
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:148) ~[na:na]
at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:129) ~[na:na]
at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297) ~[na:na]
at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:383) ~[na:na]
... 35 common frames omitted
Bug description
Authentication to Azure OpenAI performs an SSL connection to www.example.com, that fails in some cases (e.g. if a custom certificate is required).
Environment
Fails with Spring AI 2.0.0 but worked fine with Spring AI 1.x.
Steps to reproduce
We use Spring AI with LLMs from Azure OpenAI.
The Azure OpenAI authentication does not use API key but the Azure CLI (for local dev) or managed identities (when hosted on Azure).
With Spring AI 1.x it worked fine. But with Spring AI 2.0.0 it now fails because of an SSL handshake failure.
It seems to come from a connection to
https://www.example.comperformed by theazure-identitylibrary:That code is called from
org.springframework.ai.openai.setup.AzureInternalOpenAiHelper#getAzureCredential(line 40) called byorg.springframework.ai.openai.setup.OpenAiSetup#azureAuthentication(line 328).This fails in our company because some specific certificates are required to perform HTTPS connections.
The LangChain4j counterpart works fine (no SSL error):
We did not have to import the certificates for Spring AI 1.x. We also do not have to import the certificates for LangChain4j.
Moreover, connecting to
www.example.comseems quite weird.Expected behavior
Azure OpenAI authentication should work fine, and no SSL connection to
www.example.comshould be attempted.If we add the certificates to the JVM, then it works fine. But doing so for multiple applications is a problem.
Considering that (a) it worked fine with Spring AI 1.x and (b) it works fine with LangChain4j, this seems like a Spring AI bug or regression.
What do you think ? Could this be fixed ?
Sample stack trace