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 order in which the Vauban BeanManager returns the beans (CDI discovery order, stable across runs). 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 / contextDestroyed events fire through FoyChappeBoot.Mounted.fireContextInitialized() / fireContextDestroyed().
Async (AsyncContext)
Foy implements request.startAsync(), AsyncContext.dispatch(…), complete(), and setTimeout. 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();
}
});
}
}
|
A few edge cases of the 6.1 TCK around |
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.
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.
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.
WAR deployment is not supported: Foy boots embedded, programmatically. (The Arquillian container in foy-tck deploys the TCK wars, but it is test harness machinery, not a product feature.)
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 |
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) read from the servlet class.<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.