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 follows web.xml (<filter-mapping>) order or, if absent, the alphabetical order of class names — see Reference for details.

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 startAsync after dispatch are still failing — see TCK status.

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 streams over the ServletInputStream backed by the Chappe body — no in-memory copy for large uploads.

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 stacking multiple Mounted instances on the same ChappeMountPoint to serve several applications on the same connector;

  • eventually through WAR packaging — // TODO@user: document the WAR deploy command once stable.

CDI integration through Vauban

When foy-cdi-vauban is on the classpath, Foy exposes the following beans:

Bean Scope

ServletContext

@ApplicationScoped

HttpServletRequest

@RequestScoped

HttpSession

@SessionScoped (through Vauban — // TODO@user: confirm M2 availability)

Any Servlet or Filter can therefore use @Inject:

import jakarta.inject.Inject;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;

@WebServlet("/me")
public class MeServlet extends HttpServlet {
    @Inject HttpServletRequest request;
    @Inject MyBusinessService service;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        resp.getWriter().write(service.greet(request.getRemoteUser()));
    }
}

Resolution is fully build-time through Vauban — no runtime dynamic proxy, no hot-path reflection.

Application security

  • <security-constraint> from web.xml is enforced by SecurityConstraintEnforcer.

  • BasicAuthenticator covers HTTP Basic.

  • AnonymousSecurityProvider is the open fallback (development only).

  • Form-based and Digest: // TODO@user: document once stable.

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 ChappeMountPoint you can mount /app (Foy) and /api (Cassini) — each owns its contextPath, the two containers do not interact.