Foy honours the Jakarta Servlet 6.1 spec to the letter — your web.xml, your @WebServlet, your filters and listeners are ported as-is. This page lists the points where the environment differs.
|
Foy is at 0.4.0-SNAPSHOT and the official TCK passes 96.8 % over the full suite: 1659 passing / 1714 run, 7 skipped ( |
From Tomcat
| Tomcat | Foy | Note |
|---|---|---|
|
|
Programmatic configuration. The connector is Chappe; the Foy handler is mounted through the Chappe router ( |
|
Supported subset |
Read by |
|
Identical |
Detected at compile time by APT — no runtime annotation scan (descriptors and initializers are discovered at boot). |
|
✅ Implemented |
Discovered on the class path or module path, ordered per §8.2.2 and merged per §8.2.3 — see the native layout. |
Coyote (HTTP/1.1, HTTP/2) |
HTTP/1.1 + HTTP/2 + virtual threads. |
|
|
|
Implement |
JNDI lookups ( |
|
Datasources / pools are CDI Vauban beans. |
|
CDI Vauban beans |
Foy ships no JNDI tree — port each |
From Jetty (Servlet)
| Jetty | Foy | Note |
|---|---|---|
|
Chappe |
The connector is Chappe. |
|
|
WAR deploy is not supported — Foy boots embedded, programmatically. |
|
|
Likewise. |
|
❌ Not implemented |
WebSocket Servlet 6.1 planned, no date. |
Custom Jetty |
Chappe |
The |
From Undertow
| Undertow | Foy | Note |
|---|---|---|
|
|
Two-step programmatic config: Chappe for transport, Foy for Servlet. |
|
|
No explicit Servlet/Filter config — annotations + |
Undertow |
Chappe |
See Chappe Reference. |
From Helidon Servlet
No real-world migration has been documented yet — the Tomcat and Jetty tables above transpose directly (programmatic boot, Chappe transport, SecurityProvider SPI).
Known gaps
-
HTTP/2 server push — not supported;
newPushBuilder()returnsnull(push is deprecated in Servlet 6.1). -
Directory listings and
multipart/byteranges— the default servlet serves single files only (404 for a directory) and answers several ranges with the whole body. -
Security enforcement —
<security-constraint>and<login-config>are parsed but not enforced yet. -
WebSocket Servlet 6.1 / Jakarta WebSocket 2.2 — not implemented (planned, no date).
-
JSP — not supported by Foy itself; planned through a JSP-engine SPI that Ibarra (Jakarta Pages 4.0) plugs into. For dynamic pages use Cassini (REST) or HTML-emitting Servlets.
-
Form-based / Digest authentication — not implemented.
-
Multipart — fully buffered in memory; limit/threshold handling partial.
Cross-context dispatch (ServletContext.getContext) is implemented via CrossContextRegistry — strict contextPath match on the contexts deployed in the same JVM.
Behaviour changes for FoyChappeBoot embedders
Applications that already embedded Foy through FoyChappeBoot.builder() before descriptors and pluggability landed see these changes:
-
Discovery is on by default. Every
META-INF/web-fragment.xmlon the class or module path is merged and everyServletContainerInitializerthe ordering retains runs. A lone initializer on the path is enough to activate Foy, even without servlet, filter, listener or descriptor. -
Descriptor errors now fail the deployment (
ServletExceptionfrombuild()): a malformedweb-fragment.xml, two fragments with the same<name>, conflicting values between fragments that web.xml does not settle (§8.2.3), and a url-pattern mapped to two servlets (§12.2). -
<default-context-path>changes the mount path when the builder sets nocontextPath(…). -
discoverPluggability(false)opts out of the fragment and initializer discovery, for embedders that assemble the application themselves. -
New builder methods:
resourceProvider(…)replaces the source behindServletContext.getResource*;applicationRoot(URL…)declares the roots whose initializers always run, whatever the absolute ordering.
Request, response and session semantics
The request, response and session work (Servlet 6.1 sections 3, 5, 7, 9 and 10) changes these behaviours for embedders:
-
The root context path is
"", not"/".getContextPath()andServletContext.getContextPath()return the empty string for an application mounted at the root (section 3.5);contextPath("/")is still accepted as its alias. -
Context paths are validated at deploy. A context path must be empty or start with
/, must not end with/, and must not contain an empty,.or..segment, a control character, a backslash,%,;,?or#; an invalid one fails the deployment with anIllegalArgumentException. -
Default session tracking modes are
{COOKIE, URL}.encodeURLandencodeRedirectURLnow append;jsessionid=<id>when a session exists and the client did not send its id in a cookie. Restrict the modes with<tracking-mode>orSessionCookieConfig/setSessionTrackingModesif you do not want URL rewriting. -
The session store is consulted on every request carrying a session id (cookie or
;jsessionid), even when the application never callsgetSession, so that the session’s last-accessed time moves (section 7.6). A customSessionStoresees a lookup per such request. -
A container default servlet (
foy.default) is installed when no application servlet maps/. It serves the static resources of the resource provider; with the defaultClassPathResourceProviderthat is theMETA-INF/resources/tree of every root of the class loader (application roots, fragment jars, then any other jar), over HTTP. To opt out, map a servlet of your own to/, or pass aresourceProvider(…)that does not expose those roots. -
Non-canonical request paths are answered 400. A path that holds a raw backslash or control character, a malformed or encoded separator or NUL escape, a dot-only segment once decoded, or climbs above the context root with
..is refused before any filter or servlet runs (section 3.5.2); a path outside the context path is answered 404. -
WEB-INF/andMETA-INF/are refused to every client request (404), whichever servlet the path maps to (a*.jspmapping included), in any case and with trailing dots or spaces;/WEB-INFwithout a slash is a 404, not a redirect. A welcome file under these trees is never resolved. Forward, include, error and async dispatches may still target them. -
Dispatch paths are context-relative.
ServletContext.getRequestDispatcher,ServletRequest.getRequestDispatcherandAsyncContext.dispatch(String)no longer strip a leading copy of the context path: in context/app,getRequestDispatcher("/app/x")targets/app/xinside the application. -
Header values are validated.
setHeader,addHeader(and the int/date variants),setContentType,setCharacterEncodingandaddCookiethrowIllegalArgumentExceptionfor a name or value containing CR, LF, NUL or a character above U+00FF.sendRedirectpercent-encodes (UTF-8) non-ASCII characters, spaces and controls of the location, so aLocationheader never carries a line break. -
An unhandled exception answers a generic
500body (Internal Server Error); the exception and its message are logged, never echoed to the client.
Streaming, async and non-blocking I/O
The streaming, async and non-blocking I/O work (Servlet 6.1 sections 2.3.3, 3.7 and 5) changes these behaviours for embedders:
-
Servlet code runs on a Foy virtual thread, not on Chappe’s connection thread. Filters, servlets and listeners run on
foy-request-<n>. Chappe’sRequestContext.CURRENTis re-bound there, but any otherThreadLocalorScopedValuethat a Chappe filter or handler set on the connection thread is not visible to servlet code: pass such state through request attributes instead. -
Responses above the buffer size are streamed. A response larger than
getBufferSize()(8192 bytes by default) is committed at the overflowing write and sent while the servlet writes; without a declaredContent-Lengthit is chunked on HTTP/1.1 (before, the whole body was held until the servlet returned and sent with aContent-Length).flushBuffer()now sends the head and the buffered bytes at once. Set aContent-Length, or a largersetBufferSize, to keep a fixed-length body. -
An async timeout answers
500through the error-page mechanism instead of a bare503. When noAsyncListenercompletes or dispatches inonTimeout, the container raises an error dispatch with status500; an<error-page>for500now renders it. -
newPushBuilder()returnsnull. Code that pushed resources must check fornull(the specification allows it); 103 Early Hints are the replacement. -
A connection whose upload the application stopped reading is closed after the response. When a
ReadListenerstill waits for upload bytes at the end of the request, the response is delivered and the connection is then closed rather than kept alive. -
An exception after the commit aborts the response. Once the response is streaming, a late exception can no longer turn into an error page: it is logged and the body is aborted (connection aborted on HTTP/1.x, stream reset with
RST_STREAMINTERNAL_ERRORon HTTP/2), so the client sees a truncated body.
Request listeners
The request listener hardening changes these behaviours for embedders:
-
Every
requestDestroyedruns, even when one throws. The listeners are still called in reverse registration order; the firstRuntimeExceptionis rethrown once they have all run, with the later ones added as suppressed exceptions (anErrorstill propagates at once). -
A failed
requestInitializedis unwound. When a listener throws fromrequestInitialized, the listeners already initialized getrequestDestroyed, in reverse order, before the exception propagates. -
A filter failure on a path no servlet maps ends like any other failure. A
RuntimeExceptionorIOExceptionthrown by a filter there (before, only aServletException) is answered with Foy’s generic500and the request listeners getrequestDestroyed.
Common pitfalls
-
ServletContainerInitializer`s and fragments are discovered at boot.`ApplicationSources loads the initializers throughServiceLoader(META-INF/servicesorprovides) and theMETA-INF/web-fragment.xmlof every root, then orders them per §8.2.2 and §8.2.4. Component classes themselves are still never scanned. -
metadata-completeis honoured. Withmetadata-complete="true"annotations are ignored; otherwiseweb.xmland annotations are merged per Servlet 6.1 §8.2.3 (the descriptor wins on a same-name conflict). -
No runtime dynamic proxy. Frameworks that rely on it (Spring AOP, some profiling libs) won’t work as-is. Prefer APT alternatives (Vauban, Class-File API).
-
Strict Java modules. If a legacy uses reflective
setAccessible(true), it needs an explicit--add-opens— better to port the legacy to@Inject.