Undertow
spider-silk-undertow runs the same app on an embedded Undertow.
It is a separate module for the same reason spider-silk-tomcat is: a tie to one server belongs in the artifact’s name rather than behind a flag in core.
For how the three servers compare and which to pick, see Choosing a server.
Installation
dependencies {
implementation('net.benelog.spidersilk:spider-silk-undertow:1.1.0') {
exclude group: 'org.eclipse.jetty.ee10' // core's Jetty, unused here
}
}
The module depends on undertow-servlet 2.3.x, the Jakarta EE 10 line.
undertow-servlet 2.3.x is Servlet 6.0, the same level as core’s servlet API and Jetty’s ee10.
Tomcat 11.0.x is one level above, at Servlet 6.1.
undertow-core has a 2.4.x, but undertow-servlet, the half this module needs, has not followed it yet.
Running
new App()
.get("/hello/{name}", req -> WebResponse.text("Hello " + req.pathParam("name")))
.server((app, port) -> new UndertowServer(app).port(port))
.start(8080);
The UndertowServer surface mirrors JettyServer and TomcatServer:
new UndertowServer(app)
.port(8443)
.host("127.0.0.1")
.contextPath("/app")
.executor(Executors.newVirtualThreadPerTaskExecutor())
.multipart(new MultipartConfigElement(tmp, 10_485_760L, 10_485_760L, 1_048_576))
.stopTimeout(Duration.ofSeconds(20))
.shutdownHook(false)
.customizeDeployment(deployment -> deployment.setDefaultEncoding("UTF-8"))
.customizeBuilder(builder -> builder.setIoThreads(4).setWorkerThreads(64))
.start();
There are two customizers rather than three, because Undertow has only two objects worth reaching: the DeploymentInfo that describes the servlet deployment, and the Undertow.Builder that describes the server.
What is Undertow’s own
Graceful shutdown is the one place Undertow is the easiest of the three.
GracefulShutdownHandler counts the requests it has let through, so stopTimeout(…) is a wait for that count to reach zero rather than a thread pool being drained.
Because the drain counts requests, executor(…) does not interfere with it the way it does on Tomcat: swapping in the virtual-thread executor leaves the drain fully working.
Once the drain is over, stop() gives Undertow’s worker 100 milliseconds and then interrupts what still runs on it.
Undertow on its own waits for the worker with no limit, so a handler blocked on something other than I/O would hold the stop for as long as it ran.
A read waits at most 30 seconds for the client’s next bytes, and a write waits at most 30 seconds for the client to take them.
The 30 seconds are Jetty’s connector idle timeout.
Undertow sets neither timeout on its own, so a client that stalled mid-body would hold a worker thread for good.
customizeBuilder(builder → builder.setSocketOption(Options.READ_TIMEOUT, 60_000)) replaces the read timeout, and Options.WRITE_TIMEOUT replaces the write timeout.
A request body is limited by core’s bodyLimits(…) and the multipart configuration, as on every server.
The module lifts Undertow’s own limit, which refused every body over 2MB before the app ran.
A form body is the exception and is held to 2MB, Tomcat’s maxPostSize.
Undertow parses a form into parameters with no limit of its own, so a larger form is answered 413 before the app runs.
Undertow’s worker threads do not hold the JVM up on their own, so start() parks a non-daemon thread that stop() releases.
The Tomcat module needs the same non-daemon thread, and join() waits on it.
There is no sessions(boolean): an Undertow deployment always gets a session manager.
A method that silently did nothing would be worse than no method.
The session cookie is HttpOnly, and it is Secure when the request that creates the session is secure, as on Jetty and Tomcat.
A session is found by its cookie only, as on Jetty.
A ;jsessionid= in the URL is ignored, so a link that carries one cannot fix the session of whoever follows it.
Static files are still served by core’s own StaticFiles, classpath root or directory alike, so Undertow’s resource manager is left empty on purpose.
A response body that fails after the response is committed closes the connection.
Undertow on its own would end the response as if the body were whole, so the module installs a servlet exception handler that closes the connection once the response has started.
The client then sees an aborted transfer, as it does on Jetty and Tomcat.
A customizeDeployment(…) that calls setExceptionHandler(…) replaces that handler.
Logging goes through JBoss Logging, which uses slf4j when slf4j is on the classpath. An application already using slf4j therefore gets Undertow’s log lines without a bridge.