Skip to content

Jetty 12.1.x TLS connections hang after several requests with internal HTTPS self-requests #14564

@HHenrik

Description

@HHenrik

Jetty version(s)
jetty-12.1.6 | NOK
jetty-12.1.5 | NOK
jetty-12.1.0 | NOK
jetty-12.1.0.beta2 | NOK
jetty-12.1.0.beta1 | NOK
jetty-12.1.0.beta0 | NOK
jetty-12.1.0.alpha2 | NOK
jetty-12.1.0.alpha1 | NOK
jetty-12.1.0.alpha0 | OK
jetty-12.0.32 | OK
jetty-12.0.31 | OK
jetty-12.0.30 | OK
jetty-12.0.29 | OK
jetty-12.0.28 | OK
jetty-12.0.27 | OK

Jetty Environment
EE10

HTTP version
HTTP/2

Java version/vendor (use: java -version)
openjdk version "17.0.18" 2026-01-20

OpenJDK Runtime Environment (build 17.0.18+8-Ubuntu-122.04.1)

OpenJDK 64-Bit Server VM (build 17.0.18+8-Ubuntu-122.04.1, mixed mode, sharing)

Description
In a scenario where you trigger internal requests towards the jetty server from within the jetty application through localhost, you end up in a hanging state. Internal HTTPS self-requests hang after 5-12 sequential requests. The TLS handshake completes, but the request is never dispatched to the handler, eventually timing out.

This problem does not occur when using HTTP1.1 on the incoming request, its only a problem with HTTP/2. HTTP version on the internal request does not matter.

How to reproduce?
See attached jetty application. It is reproduced on every run.

jetty-reproduction.tar.gz

  1. Generate test certificates and build the SBT project:

./generate-certs.sh

sbt clean assembly

  1. Start the Jetty server listening on HTTPS (port 8443). Run it using:

java -jar target/scala-2.13/jetty-bug-reproduction-assembly-1.0.jar

  1. Send many requests (1-20). Run the test script:

./test.sh

Jetty server code included in above attachment:

package com.example.jetty

import org.eclipse.jetty.server._
import org.eclipse.jetty.util.ssl.SslContextFactory
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory
import org.eclipse.jetty.util.Callback

import java.net.http.{HttpClient, HttpRequest, HttpResponse}
import java.net.URI
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
import java.security.KeyStore
import java.io.FileInputStream

object MinimalReproduction {

  def main(args: Array[String]): Unit = {
    val server = new Server()

    val ssl = new SslContextFactory.Server()
    ssl.setKeyStorePath("tls/keystore.jks")
    ssl.setKeyStorePassword("changeit")
    ssl.setTrustStorePath("tls/truststore.jks")
    ssl.setTrustStorePassword("changeit")

    val http2 = new HTTP2ServerConnectionFactory()
    val alpn = new ALPNServerConnectionFactory()
    alpn.setDefaultProtocol("http/1.1")

    val connector = new ServerConnector(server,
      new SslConnectionFactory(ssl, alpn.getProtocol),
      alpn, http2, new HttpConnectionFactory())
    connector.setPort(8443)
    server.addConnector(connector)

    val ks = KeyStore.getInstance("JKS")
    ks.load(new FileInputStream("tls/keystore.jks"), "changeit".toCharArray)
    val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
    kmf.init(ks, "changeit".toCharArray)

    val ts = KeyStore.getInstance("JKS")
    ts.load(new FileInputStream("tls/truststore.jks"), "changeit".toCharArray)
    val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
    tmf.init(ts)

    val ctx = SSLContext.getInstance("TLS")
    ctx.init(kmf.getKeyManagers, tmf.getTrustManagers, null)

    val client = HttpClient.newBuilder().sslContext(ctx).build()

    server.setHandler(new Handler.Abstract {
      override def handle(request: Request, response: Response, callback: Callback): Boolean = {
        request.getHttpURI.getPath match {
          case "/external" =>
            println("External request - making internal call")
            val req = HttpRequest.newBuilder().uri(URI.create("https://localhost:8443/internal")).GET().build()
            val resp = client.send(req, HttpResponse.BodyHandlers.ofString())
            response.setStatus(200)
            response.write(true, java.nio.ByteBuffer.wrap(s"Success: ${resp.body()}".getBytes), callback)
          case "/internal" =>
            response.setStatus(200)
            response.write(true, java.nio.ByteBuffer.wrap("OK".getBytes), callback)
          case _ =>
            response.setStatus(404)
            callback.succeeded()
        }
        true
      }
    })

    server.start()
    println("Server started on port 8443")
    server.join()
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugFor general bugs on Jetty side

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions