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(), a dispatch(…​), a timeout or an error; onStartAsync fires on the listeners when a dispatched target calls startAsync() again, and onComplete fires once, when the last cycle ends.

  • Timeout. The default timeout is 30 s (setTimeout(0) disables it). On timeout the listeners get onTimeout; if none of them calls complete() or dispatch(…​), the container raises an error dispatch with status 500, 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' onError and 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.start that keeps writing afterwards gets an IOException (through the PrintWriter, the characters are dropped and checkError() 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).

ReadListener.onError is not called when the async cycle times out: the timeout goes to the `AsyncListener`s and the error dispatch only. The specification does not require it and the TCK does not test it.

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(), or flush() on the stream or writer;

  • the declared Content-Length written 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. onDataAvailable fires when request bytes are buffered; read while isReady() is true, then return. onAllDataRead fires once at the end of the body, onError if the upload fails (a client that resets the connection, a body cut short). Callbacks never overlap.

  • Writing. Write while isReady() is true; a write never blocks. When isReady() answers false, onWritePossible fires again once the client has taken the pending bytes. onError fires if the client goes away.

  • A read() when no data is available, or a write() after isReady() answered false (while bytes still wait for the client), throws IllegalStateException, 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 WriteListener is not resumed after an AsyncContext.dispatch(…​): the dispatched target runs with a blocking stream (the listener stays set, but onWritePossible is 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 ReadListener is 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, a ThreadLocal of the request thread). Compute the values in the servlet and let the supplier return them.

  • On HTTP/1.1, setting a Content-Length after setTrailerFields makes the body fixed-length: the trailers are then silently dropped.

  • On HTTP/1.1, a response sent with Connection: close is not chunked, so its trailers are dropped.

  • reset() keeps the supplier: call setTrailerFields(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.

  • upgrade throws IOException on HTTP/1.0 and HTTP/2, and IllegalStateException after startAsync() or when called twice; startAsync() after upgrade() throws IllegalStateException too.

  • A status other than 101 is 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 a 500; the handler is then never initialised, and destroy() is not called.

  • WebConnection.getInputStream() and getOutputStream() are blocking by default and support ReadListener and WriteListener.

  • 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 usual out.write(lastFrame); connection.close(); from onWritePossible), 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 (MultipartParser reads the whole body; no streaming parse, no temp-file spill to location). @MultipartConfig limit/threshold handling is partial: 8 of the 14 PartTests/Part1Tests TCK cases still fail (upload limits, thresholds) — see TCK status. Keep uploads small.

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 no forward.* 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, the include. 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.

  • forward and include may reach resources under WEB-INF/ and META-INF/, which a client request cannot.

  • getRequestDispatcher with a path above the context root returns null.

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):

  • GET and HEAD (same headers, no body) are served, OPTIONS answers an Allow: GET, HEAD, OPTIONS header, any other method answers 405. A directory is 404: there is no listing.

  • Content-Type comes from mime-mapping and the built-in table, Last-Modified from the file time (or the jar entry time) and a weak ETag from length and time. If-None-Match wins over If-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 (no multipart/byteranges). If-Range is honoured by date.

  • A client request cannot reach WEB-INF/ or META-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-INF without a slash is not redirected. A forward, include, async or error dispatch can, for instance an error page kept under WEB-INF/.

  • A custom ResourceProvider may implement metadata(path) to give the length and the modification time without opening the resource. ClassPathResourceProvider does, 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

WEB-INF/classes

the application classes (processed by foy-processor)

WEB-INF/lib/*.jar

the Maven dependencies on the class path or module path

WEB-INF/web.xml

WEB-INF/web.xml, else META-INF/web.xml, looked up through the class loader (or Builder.webXml(InputStream), which wins)

WEB-INF/lib/x.jar!/META-INF/web-fragment.xml

META-INF/web-fragment.xml in any jar or directory root visible to the class loader

static content at the WAR root

META-INF/resources/ in any jar or directory root

ServletContainerInitializer

META-INF/services/jakarta.servlet.ServletContainerInitializer, or provides …​ with in a module-info.java; found through ServiceLoader

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 a web.xml without 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 default ClassPathResourceProvider, which serves META-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 the WEB-INF/ and META-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(…​) sets ServletContext.getVirtualServerName() (default vidocq).

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 ServletContext.getResource* and getRealPath (which answers a path only for a resource held in a directory). Security constraints are parsed but not enforced yet.

Virtual hosts and WAR deployment

Foy can be started:

  • programmatically through FoyChappeBoot.builder() — one container per contextPath;

  • by mounting multiple Mounted handlers on the same Chappe Router (one Router.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 path is needed without CDI. The vauban-processor and foy-cdi-vauban paths are for applications using foy-cdi-vauban; they are build-time only.

  • foy-cdi-vauban must be on the processor path too, not only on the compile class path: once annotationProcessorPaths is set, javac looks for processors and their service providers on those entries alone, so vauban-processor finds the FoyWebExtension build compatible extension only there. Without it the extension never runs: unscoped web components do not become @Dependent beans, so Foy never sees them, and the others get no CdiWebComponents index entry. Foy still discovers such a bean by walking the Servlet, Filter and EventListener beans, but logs one WARNING per 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 your module-info.java.

  • Spec misuse (for example a @WebServlet class that is not a Servlet) fails the compilation.

  • In a named module, the name-based companion lookup uses publicLookup: either declare provides 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 needs opens.

  • 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 ServletContext, HttpServletRequest or HttpSession as injectable CDI beans — there are no @ApplicationScoped / @RequestScoped / @SessionScoped producers for the Servlet API objects. Use the request/response parameters passed to service()/doGet().

NEW Your own @RequestScoped beans do work: Foy activates the CDI request context for each request, under Vauban or any other CDI container. @SessionScoped beans do not: the session context is not activated (Other CDI containers).

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: SecurityConstraintEnforcer applies @ServletSecurity (@HttpConstraint, @HttpMethodConstraint, EmptyRoleSemantic.DENY) taken from the component descriptor (generated at build time, or decoded from the class bytes). <security-constraint> in web.xml is not parsed.

  • BasicAuthenticator covers HTTP Basic (8 of the 14 secbasic TCK tests still fail — see TCK status).

  • AnonymousSecurityProvider is 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.