This page collects recipes for using Foy beyond "hello world". It follows the canonical chapters of Jakarta Servlet 6.1.
Filters
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class RequestIdFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
var id = java.util.UUID.randomUUID().toString();
req.setAttribute("requestId", id);
((jakarta.servlet.http.HttpServletResponse) resp).addHeader("X-Request-Id", id);
chain.doFilter(req, resp);
}
}
Filter ordering: for @WebFilter beans the Servlet 6.1 spec (§6.2.4) leaves the order unspecified — Foy uses the discovery order: the CdiWebComponents index order first, then any web-annotated beans missing from the indexes (BeanManager order). Filters declared in web.xml follow the <filter-mapping> document order and are contributed after the annotation-discovered ones. FilterRegistry preserves that registration order; the chain executes in list order.
Listeners
@WebListener covers ServletContextListener, HttpSessionListener, ServletRequestListener and their *AttributeListener variants. All are enrolled by ListenerRegistry from beans discovered by Vauban.
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
@WebListener
public class StartupHook implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("started-at", java.time.Instant.now());
}
}
contextInitialized fires when FoyChappeBoot.Builder.build() deploys the web application; contextDestroyed fires when the returned FoyChappeBoot.Mounted is closed (Mounted is AutoCloseable).
Async (AsyncContext)
Foy implements request.startAsync(), AsyncContext.start(…), dispatch(…), complete(), setTimeout and AsyncListener. Each request runs on a virtual thread — switching to async carries no extra cost (no platform pool to protect).
@WebServlet(value = "/long", asyncSupported = true)
public class LongServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
var ctx = req.startAsync();
ctx.setTimeout(5_000);
Thread.startVirtualThread(() -> {
try {
Thread.sleep(2_000);
resp.getWriter().write("done");
ctx.complete();
} catch (Exception e) {
ctx.complete();
}
});
}
}
The async lifecycle follows Servlet 6.1 section 2.3.3.3:
-
One cycle at a time. A cycle ends with
complete(), adispatch(…), a timeout or an error;onStartAsyncfires on the listeners when a dispatched target callsstartAsync()again, andonCompletefires once, when the last cycle ends. -
Timeout. The default timeout is 30 s (
setTimeout(0)disables it). On timeout the listeners getonTimeout; if none of them callscomplete()ordispatch(…), the container raises an error dispatch with status500, which goes through the<error-page>mechanism, then completes the request. -
Errors. An exception escaping the servlet after
startAsync(), or an async dispatch target, reaches the listeners'onErrorand then the error dispatch, as for a timeout. -
One writer at a time. When a cycle ends, the container takes the response output back before any listener runs: a thread started by
AsyncContext.startthat keeps writing afterwards gets anIOException(through thePrintWriter, the characters are dropped andcheckError()turns true). A thread the application manages itself (its own executor) is not known to the container and cannot be fenced the same way (BUG-20261010-02, partial).
|
|
Streaming and response buffering
The response is buffered up to getBufferSize() bytes (8192 by default, changed with setBufferSize before any content is written). While the content fits, the response is not committed: reset(), resetBuffer(), sendError and an error page can still replace it, and a response that ends there is sent whole with a Content-Length (chunked instead when trailer fields are set).
The response is committed, and its head sent, at the first of:
-
a write that would overflow the buffer;
-
flushBuffer(), orflush()on the stream or writer; -
the declared
Content-Lengthwritten in full.
From then on the body goes to the client while the servlet keeps writing: small writes are coalesced in the buffer and pushed when it fills, on a flush, and at the end of the request. Without a declared Content-Length, an HTTP/1.1 body is sent chunked; on HTTP/2 it is sent as DATA frames. A client that reads slowly slows the writing servlet down (the pipe between the servlet and the connection is bounded): memory stays bounded whatever the body size.
An exception that escapes after the commit cannot change the response any more: it is logged and the body is aborted (connection aborted on HTTP/1.x, stream reset with RST_STREAM INTERNAL_ERROR on HTTP/2), so the client sees a truncated body rather than a complete-looking one.
Non-blocking I/O (ReadListener, WriteListener)
On an async-started request (or an upgraded connection, see below), ServletInputStream.setReadListener and ServletOutputStream.setWriteListener switch the stream to non-blocking mode (Servlet 6.1 section 3.7):
-
Reading.
onDataAvailablefires when request bytes are buffered; read whileisReady()istrue, then return.onAllDataReadfires once at the end of the body,onErrorif the upload fails (a client that resets the connection, a body cut short). Callbacks never overlap. -
Writing. Write while
isReady()istrue; a write never blocks. WhenisReady()answersfalse,onWritePossiblefires again once the client has taken the pending bytes.onErrorfires if the client goes away. -
A
read()when no data is available, or awrite()afterisReady()answeredfalse(while bytes still wait for the client), throwsIllegalStateException, as the specification requires.
@WebServlet(value = "/upload", asyncSupported = true)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
AsyncContext ac = req.startAsync();
ServletInputStream in = req.getInputStream();
in.setReadListener(new ReadListener() {
long total;
@Override public void onDataAvailable() throws IOException {
byte[] buf = new byte[8192];
while (in.isReady()) {
int n = in.read(buf);
if (n < 0) break;
total += n;
}
}
@Override public void onAllDataRead() throws IOException {
resp.getWriter().print(total);
ac.complete();
}
@Override public void onError(Throwable t) { ac.complete(); }
});
}
}
Limits:
-
A
WriteListeneris not resumed after anAsyncContext.dispatch(…): the dispatched target runs with a blocking stream (the listener stays set, butonWritePossibleis not called again). Set the listener in the cycle that writes. -
When a cycle ends with bytes still waiting for the client, the container sends them in blocking mode at the end of the request, in order. Foy sets no bound of its own on that wait: a client that never reads keeps the request thread until chappe’s write timeout (30 s per blocked write by default) closes the connection (BUG-20261010-03).
-
If a
ReadListeneris still waiting for upload bytes when the request ends (the client went silent mid-body), the response is sent first and the connection is then closed instead of being kept alive: the unread part of the body cannot be skipped safely. An unread body that the client keeps sending is drained by Chappe as before.
Response trailers
HttpServletResponse.setTrailerFields(Supplier<Map<String, String>>) sends trailer fields after the body: on HTTP/1.1 in the trailer section of a chunked body, on HTTP/2 in a final HEADERS frame. The supplier is called once, after the last byte of the body. It is refused (IllegalStateException) on HTTP/1.0, once the response is committed, and on HTTP/1.1 when a Content-Length is already declared. A failing supplier is logged and no trailer is sent. Names that RFC 9110 section 6.5.1 forbids in trailers (framing, routing, authentication, Set-Cookie, …) are dropped.
Limits:
-
The supplier runs on chappe’s connection thread after
service()has returned: it must not use request-scoped state (the request, the session, a CDI request scope, aThreadLocalof the request thread). Compute the values in the servlet and let the supplier return them. -
On HTTP/1.1, setting a
Content-LengthaftersetTrailerFieldsmakes the body fixed-length: the trailers are then silently dropped. -
On HTTP/1.1, a response sent with
Connection: closeis not chunked, so its trailers are dropped. -
reset()keeps the supplier: callsetTrailerFields(null)to remove it.
HTTP Upgrade (HttpUpgradeHandler)
HttpServletRequest.upgrade(Class) hands the connection to an HttpUpgradeHandler once the servlet returns (Servlet 6.1 section 2.3.3.5). The application sets the 101 status and the Upgrade/Connection headers itself; the container sends that head, discards any body written to the response, ends the request lifecycle (requestDestroyed) and calls init(WebConnection).
@WebServlet("/echo")
public class EchoUpgradeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
resp.setStatus(101);
resp.setHeader("Upgrade", "echo");
resp.setHeader("Connection", "Upgrade");
req.upgrade(EchoHandler.class);
}
}
-
The handler is instantiated by the container, through the same component factory as servlets, filters and listeners (CDI included); an instantiation failure is a
ServletException. -
upgradethrowsIOExceptionon HTTP/1.0 and HTTP/2, andIllegalStateExceptionafterstartAsync()or when called twice;startAsync()afterupgrade()throwsIllegalStateExceptiontoo. -
A status other than
101is sent as set, with a warning in the log. A status or header that cannot go into the upgrade head (an invalid header name or value) is logged as a warning and answered with a500; the handler is then never initialised, anddestroy()is not called. -
WebConnection.getInputStream()andgetOutputStream()are blocking by default and supportReadListenerandWriteListener. -
destroy()is called once, when the connection ends:WebConnection.close(), a read or write error, or the undeployment of the application (which closes every upgraded connection it still holds). -
WebConnection.close()lets a non-blocking write still in flight reach the wire first (the usualout.write(lastFrame); connection.close();fromonWritePossible), waiting at most 2 seconds: past that, a peer that does not read gets the connection closed anyway. -
A handler that neither reads nor closes keeps the connection open until a write fails or the application is undeployed: there is no idle timeout once a connection is upgraded.
WebSocket is not built on top of this yet (Jakarta WebSocket is not implemented).
HTTP/2 server push
Not supported. HttpServletRequest.newPushBuilder() returns null, which Servlet 6.1 allows when push is unavailable; push is deprecated in Servlet 6.1 in favour of 103 Early Hints. Chappe has neither an h2c Upgrade nor a PUSH_PROMISE writer.
File upload (@MultipartConfig)
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
@WebServlet("/upload")
@MultipartConfig(maxFileSize = 10 * 1024 * 1024)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
Part part = req.getPart("file");
try (var in = part.getInputStream()) {
in.transferTo(java.nio.file.Files.newOutputStream(
java.nio.file.Path.of("/tmp", part.getSubmittedFileName())));
}
resp.setStatus(204);
}
}
|
Multipart parsing is fully buffered in memory ( |
Sessions
The internal SessionManager is backed by InMemorySessionStore by default. Timeout is driven by FoyChappeBoot.builder().sessionTimeoutSeconds(…) or by <session-config> in web.xml; a ServletContext.setSessionTimeout(…) call from a ServletContainerInitializer or a ServletContextListener overrides both.
Expired sessions are invalidated with their listeners (sessionDestroyed, then valueUnbound/attributeRemoved), either when a request asks for them or by a reaper running on a virtual thread every min(60 s, max(1 s, timeout / 2)). A session used by an in-flight request never expires, and getLastAccessedTime() is the time of the previous request. On undeploy the reaper stops and every live session is invalidated the same way: Foy does not persist sessions across deployments, and once undeploy started getSession(true) throws IllegalStateException. A <session-timeout> (or setSessionTimeout) of zero or less means that sessions never expire.
changeSessionId() keeps the session’s attributes, fires HttpSessionIdListener.sessionIdChanged and sends the new session cookie. The default tracking modes are COOKIE and URL (a <tracking-mode> or setSessionTrackingModes replaces them), and they also govern input: the session cookie is read only when COOKIE is effective and then wins over a ;jsessionid= path parameter, which is read only without a session cookie and when URL is effective. When the effective tracking modes include URL, encodeURL/encodeRedirectURL add ;jsessionid=<id> to URLs of the application while the client does not send the session cookie. Without COOKIE in the tracking modes, no session cookie is sent. On a secure request the session cookie is Secure, unless the application set the flag through SessionCookieConfig.setSecure or <secure>.
For a distributed or persistent session, implement io.vidocq.foy.spi.session.SessionStore (see Reference).
RequestDispatcher: forward and include
@WebServlet("/router")
public class Router extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
var rd = req.getRequestDispatcher("/target?key=42");
rd.forward(req, resp); // or rd.include(req, resp);
}
}
ForwardedRequest and IncludedRequest inject the standard attributes (jakarta.servlet.forward., jakarta.servlet.include.) and reuse the Chappe routing. The semantics, all covered by the TCK requestdispatcher and srlistener suites:
-
The query string of the target is merged with the original parameters: the target’s values come first, the original ones follow.
-
getNamedDispatcher(name)reaches every registered servlet, mapped or not, and sets noforward.*attribute. -
A nested forward keeps the attributes of the first forward (the original request), and the
forward.attributes are visible only to the forwarded target, theinclude.ones only to the included target. -
An exception thrown by the target propagates unchanged to the caller. After a forward returns, the response is closed: whatever the forwarding servlet writes afterwards is dropped.
-
forwardandincludemay reach resources underWEB-INF/andMETA-INF/, which a client request cannot. -
getRequestDispatcherwith a path above the context root returnsnull.
Filter mappings
A filter is chained by URL pattern first, then by servlet name (<filter-mapping><servlet-name>, addMappingForServletNames), each group in registration order (Servlet 6.1 §6.2.4). A filter matched both ways runs once. The servlet name of the container default servlet is foy.default, so a filter can be mapped to static content by name.
Default servlet and static content
Foy deploys a container default servlet, named foy.default, mapped to / unless the application maps something to / itself. It is not an application registration: ServletContext.getServletRegistrations() does not list it. It serves the static resources of the application, the ones ServletContext.getResource reaches (META-INF/resources/ of the application roots and fragment jars, or the ResourceProvider set on the builder):
-
GETandHEAD(same headers, no body) are served,OPTIONSanswers anAllow: GET, HEAD, OPTIONSheader, any other method answers 405. A directory is 404: there is no listing. -
Content-Typecomes frommime-mappingand the built-in table,Last-Modifiedfrom the file time (or the jar entry time) and a weakETagfrom length and time.If-None-Matchwins overIf-Modified-Since; both answer 304. -
A single
Range(bytes=a-b,a-,-n) answers 206, an unsatisfiable one 416. Several ranges answer 200 with the whole body (nomultipart/byteranges).If-Rangeis honoured by date. -
A client request cannot reach
WEB-INF/orMETA-INF/(404, in any case and with trailing dots or spaces), whichever servlet the path would map to: the request bridge refuses it before mapping, and/WEB-INFwithout a slash is not redirected. A forward, include, async or error dispatch can, for instance an error page kept underWEB-INF/. -
A custom
ResourceProvidermay implementmetadata(path)to give the length and the modification time without opening the resource.ClassPathResourceProviderdoes, and never opens a jar connection per request.
// META-INF/resources/index.html is served at /index.html
// map a servlet to "/" and it replaces the default servlet.
@WebServlet("/")
public class Fallback extends HttpServlet { /* ... */ }
A resource larger than the response buffer is streamed to the client while it is read, with its Content-Length when nothing was written to the response before (see Streaming and response buffering).
Welcome files
<welcome-file-list> is applied by the default servlet only. For a request to a directory path ending with / (the context root included), Foy tries each welcome file in declaration order: first as a static resource, then as a servlet mapping (/dir/index.jsp can be a servlet mapped to *.jsp). A welcome file under WEB-INF/ or META-INF/ is never resolved, since the welcome re-entry is a client request. The first match wins, and getRequestURI keeps the original URI while getServletPath, getPathInfo and getHttpServletMapping describe the target.
A directory request without a trailing slash that has a welcome file is redirected (302) to the slash form. The Location is built from the re-encoded canonical path, never from the raw one, so //evil.example/ cannot become a protocol-relative redirect; it goes through encodeRedirectURL, so a session tracked by URL is kept. An application servlet mapped to /, or an exact, prefix or extension servlet, handles the path itself and no welcome file is looked up.
Request path canonicalisation
Before any mapping (servlet, filter, welcome file, default servlet), Foy canonicalises the request path as Servlet 6.1 §3.5.2 requires: path parameters are removed from each segment (;jsessionid= included), the path is percent-decoded exactly once as UTF-8, then normalised (repeated slashes collapsed, . and .. resolved). getRequestURI stays raw and encoded, getServletPath and getPathInfo are decoded.
A request answers 400 Bad Request before any filter or servlet runs when the path:
-
has an encoded separator or NUL (
%2F,%5C,%00, any case); -
has a malformed percent escape or invalid UTF-8;
-
holds a raw backslash, a control character or a non-ASCII character (a stricter choice than the grammar requires: send non-ASCII names percent-encoded);
-
has a segment made only of dots and spaces (
…,..%20), or a..climbing above the context root.
A path outside the context path (/app2/x for context /app) is 404; the context path is stripped on a segment boundary. OPTIONS * is passed unchanged to the default servlet. The context path configured on the builder is validated at deployment; the root context is the empty string everywhere (ServletContext.getContextPath(), HttpServletRequest.getContextPath(), the session cookie path is /).
Error pages
<error-page> entries match an exception type (the class hierarchy is walked first, then, for a ServletException, its root cause) or a status code. The error attributes jakarta.servlet.error.status_code, message, exception, exception_type, request_uri, servlet_name and, new in 6.1, error.query_string are set on the error dispatch, which uses DispatcherType.ERROR. If the response is already committed when an exception escapes, no error page is dispatched, the committed content stands and the exception is logged at ERROR.
Response semantics
sendError and sendRedirect reset the buffer and the declared Content-Length and commit the response. setStatus, headers, setLocale, setContentType and charset changes are ignored once the response is committed (on buffer overflow, after flushBuffer, or once the declared Content-Length is reached; see Streaming and response buffering). A cookie with a Max-Age also carries an Expires attribute (RFC 6265). The default character encoding follows <response-character-encoding>, then the ISO-8859-1 default, and a Content-Type charset or locale-encoding mapping overrides it until getWriter is called.
Legacy deployment descriptors
web.xml files written for Servlet 2.2 and 2.3 (with a <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" …>) are accepted. The DOCTYPE is parsed but never resolved: no external DTD is fetched, no external entity is read and an entity reference in the content is rejected, so the descriptor cannot be used for XXE or entity-expansion attacks. The DOCTYPE implies the version: a descriptor older than 2.5 is metadata-complete (annotations and fragments are ignored), and for versions before 2.4 a url-pattern without a leading slash is accepted and given one.
Native layout: descriptors, fragments and resources
A native Foy application has no WAR: it is a set of jars (or directories) on the class path or module path. Each WAR concept has a native counterpart:
| WAR concept | Native Foy |
|---|---|
|
the application classes (processed by |
|
the Maven dependencies on the class path or module path |
|
|
|
|
static content at the WAR root |
|
|
|
Discovery is on by default and is driven by FoyChappeBoot.builder():
-
discoverPluggability(false)turns off the discovery of both the fragments and the initializers, for embedders that assemble the application themselves. -
applicationRoot(URL…)declares jars or directories as the application itself: their initializers always run, whatever the absolute ordering. A root holding aweb.xmlwithout a fragment, and the roots of the CDI-discovered annotated components, are application roots without being declared. -
contextPath(…)wins; otherwise web.xml’s<default-context-path>applies, else/. The element is web.xml-only: a fragment’s is ignored with a warning. -
resourceProvider(…)replaces the defaultClassPathResourceProvider, which servesMETA-INF/resources/of the application roots, then of the ordered fragments' jars, then of any other root. It rejects empty and dot segments and never follows a symbolic link out of the root.getResource*does reach theWEB-INF/andMETA-INF/subtrees of the resource root, as the spec allows; the default servlet refuses them for a client request (see Default servlet and static content). -
virtualServerName(…)setsServletContext.getVirtualServerName()(defaultvidocq).
Fragments are ordered per Servlet 6.1 §8.2.2: an <absolute-ordering> in web.xml replaces any <ordering> in the fragments; without it, <before>/<after> constraints and <others/> apply. Initializers run in ServiceLoader order, restricted to the roots the ordering retains (§8.2.4). A jar excluded by an absolute ordering contributes neither its fragment, nor its annotations, nor its initializers, nor its META-INF/resources/ (in no lookup tier, the "any other root" fallback included), nor its classes to a @HandlesTypes initializer (its class index is not read and it is not scanned).
|
Static content is served over HTTP by the container default servlet (see Default servlet and static content) and is also reachable through |
Virtual hosts and WAR deployment
Foy can be started:
-
programmatically through
FoyChappeBoot.builder()— one container per contextPath; -
by mounting multiple
Mountedhandlers on the same ChappeRouter(oneRouter.Builder.mount(prefix, handler)per contextPath) to serve several applications on the same connector.
Runtime WAR deployment is not supported: Foy boots embedded, programmatically, from the native layout above. (The Arquillian container in foy-tck deploys the TCK wars, but it is test harness machinery, not a product feature.)
Build-time code generation
Add foy-processor to the compiler’s annotation processor path. It generates, at compile time, a companion class for every web component, so Foy needs neither classpath scanning nor reflection to instantiate them. foy-processor is a build-time tool with zero dependency (it requires only java.compiler); it never reaches the application.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths combine.children="append">
<path>
<groupId>io.vidocq.foy</groupId>
<artifactId>foy-processor</artifactId>
<version>${foy.version}</version>
</path>
<!-- CDI users: also let vauban-processor run the Foy build compatible extension -->
<path>
<groupId>io.vidocq.vauban</groupId>
<artifactId>vauban-processor</artifactId>
<version>${vauban.version}</version>
</path>
<path>
<groupId>io.vidocq.foy</groupId>
<artifactId>foy-cdi-vauban</artifactId>
<version>${foy.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
-
Only the first
pathis needed without CDI. Thevauban-processorandfoy-cdi-vaubanpaths are for applications usingfoy-cdi-vauban; they are build-time only. -
foy-cdi-vaubanmust be on the processor path too, not only on the compile class path: onceannotationProcessorPathsis set, javac looks for processors and their service providers on those entries alone, so vauban-processor finds theFoyWebExtensionbuild compatible extension only there. Without it the extension never runs: unscoped web components do not become@Dependentbeans, so Foy never sees them, and the others get noCdiWebComponentsindex entry. Foy still discovers such a bean by walking theServlet,FilterandEventListenerbeans, but logs oneWARNINGper class when another archive does carry an index. -
In a named Java Module, the processor suggests (as a compiler note) the
provides io.vidocq.foy.spi.gen.WebComponent with …clause to add to yourmodule-info.java. -
Spec misuse (for example a
@WebServletclass that is not aServlet) fails the compilation. -
In a named module, the name-based companion lookup uses
publicLookup: either declareprovides io.vidocq.foy.spi.gen.WebComponent with …(as the processor note suggests; no export needed) or export the package. Otherwise resolution falls to the Class-File tier, which then needsopens. -
Classes that were not processed (third-party jars) still work: Foy decodes their annotations from the class bytes. In a named module this needs
opens <package> to io.vidocq.foy.core; class-path code needs nothing. See Component resolution tiers.
CDI integration through Vauban
foy-cdi-vauban is a thin bridge: its single class, FoyVaubanBootstrap, hands the current Vauban BeanManager to FoyChappeBoot.
|
Foy does not (yet) expose NEW Your own |
Servlet, Filter and Listener instances are resolved through the BeanManager, so they can @Inject your application beans:
import jakarta.inject.Inject;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
@WebServlet("/me")
public class MeServlet extends HttpServlet {
@Inject MyBusinessService service;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
resp.getWriter().write(service.greet(req.getRemoteUser()));
}
}
Resolution is fully build-time through Vauban — no runtime dynamic proxy, no hot-path reflection.
Application security
-
Constraints are annotation-driven:
SecurityConstraintEnforcerapplies@ServletSecurity(@HttpConstraint,@HttpMethodConstraint,EmptyRoleSemantic.DENY) taken from the component descriptor (generated at build time, or decoded from the class bytes).<security-constraint>inweb.xmlis not parsed. -
BasicAuthenticatorcovers HTTP Basic (8 of the 14secbasicTCK tests still fail — see TCK status). -
AnonymousSecurityProvideris the open fallback (development only). -
Form-based and Digest authentication are not implemented.
For a custom provider, implement io.vidocq.foy.spi.security.SecurityProvider and register it as a CDI bean.
Cohabitation with Cassini
Foy and Cassini share the same Chappe runtime. On the same Chappe Router you can mount /app (Foy) and /api (Cassini) — each owns its contextPath, the two containers do not interact.