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 98.7 % over the full suite: 1692 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. |
|
|
|
Declare users with |
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, IdentityStore 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. -
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.
-
CLIENT-CERT deferred, DIGEST not supported — both fail closed (
403). -
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 header name that is not an RFC 9110 token (empty, a space,:, a control or non-ASCII character), and for a value containing a control character other than HTAB (CR, LF, NUL, U+0001 to U+001F, DEL) or a character above U+00FF, the rule Chappe applies on the wire (CHAPPE-008); SP, HTAB, U+0080 to U+00FF and empty values stay legal, and the message never echoes the value. An invalid trailer field is dropped with aWARNING.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.
Security
The security work (Servlet 6.1 chapter 13) changes these behaviours for embedders:
-
SecurityProviderandAuthenticatedUserare removed (API break), replaced byIdentityStoreandIdentity, with no deprecated bridge. The password now comes as achar[]that Foy wipes after the call, the result carries aPrincipaland the roles, and the store is given toFoyChappeBootinstead ofDeployOptions.withSecurityProvider:// Before SecurityProvider provider = (user, password) -> myCheck(user, password) .map(roles -> new AuthenticatedUser(user, roles)); DeployOptions options = DeployOptions.defaults(loader).withSecurityProvider(provider); // After FoyChappeBoot.builder().beanManager(bm) .identityStore((user, password) -> myCheck(user, password).map(roles -> Identity.of(user, roles))) .build(); // or, for a fixed set of users FoyChappeBoot.builder().beanManager(bm).user("alice", password, "admin").build(); -
web.xml
security-constraint`s are now enforced, as are `deny-uncovered-http-methods andtransport-guarantee. A resource that answered before may now answer401or403. A CONFIDENTIAL constraint over plain HTTP answers403, or302whenconfidentialPortis set; behind a TLS-terminating reverse proxyisSecure()isfalseand the redirect loops. -
Annotation constraints apply per url-pattern, and the descriptor wins on a pattern it names exactly.
-
The BASIC realm is
login-config/realm-nameorRestricted, no longervidocq. -
isUserInRolefollowssecurity-role-refof the executing servlet. -
login()sets the configured mechanism as auth type, rotates the session id and, under FORM, keeps the identity in the session (creating one). -
logout()keeps the session: it clears the identity only. Invalidate the session to drop its attributes. -
An unsupported
auth-methodnow answers403on protected resources (CLIENT-CERT, DIGEST, anything else). -
setServletSecurityreturns the descriptor-constrained patterns and throwsIllegalStateExceptionafter initialisation. -
ServletContext.declareRolesis implemented. -
Without users or a store, every login fails, and a context built outside the deployer refuses every request: security fails closed.
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.