Consolidated technical reference. Use this page as an anchor for external links (issues, RFCs, other Antora sites).

Maven artefacts

Artefact Role

io.vidocq.foy:foy-api:0.4.0-SNAPSHOT

Public SPI: SessionStore, SecurityProvider, AuthenticatedUser. No dependency outside jakarta.servlet-api.

io.vidocq.foy:foy-core:0.4.0-SNAPSHOT

Servlet 6.1 engine — dispatcher, filter chain, sessions, error pages, listeners, security, web.xml parser, multipart, and the Chappe bridge (ChappeServletBridge, package io.vidocq.foy.internal.bridge).

io.vidocq.foy:foy-chappe:0.4.0-SNAPSHOT

Bootstrap on the Chappe transport (FoyChappeBoot, which builds the foy-core bridge).

io.vidocq.foy:foy-cdi-vauban:0.4.0-SNAPSHOT

CDI bridge to Vauban (FoyVaubanBootstrap).

io.vidocq.foy:foy-processor:0.4.0-SNAPSHOT

Build-time annotation processor (no runtime role, never shipped in the application). Generates the X$$FoyComponent companions and the META-INF indexes. Zero dependency: it requires only the JDK java.compiler Java Module. Put it on annotationProcessorPaths, not on the class path.

io.vidocq.foy:foy-tck

Arquillian harness for the official TCK — in-reactor, gated behind the tck Maven profile (a plain mvn install neither downloads nor runs anything TCK-related; see TCK status).

Java modules and exports

module io.vidocq.foy.api {
    requires transitive jakarta.servlet;
    requires static jakarta.annotation;

    exports io.vidocq.foy.spi.session;    // SessionStore
    exports io.vidocq.foy.spi.security;   // SecurityProvider, AuthenticatedUser
    exports io.vidocq.foy.spi.gen;        // WebComponent, WebComponentDescriptor
    exports io.vidocq.foy.spi.cdi;        // CdiWebComponents
}
module io.vidocq.foy.core {
    requires transitive io.vidocq.foy.api;
    requires transitive jakarta.servlet;
    requires static jakarta.cdi;
    requires static jakarta.annotation;
    requires java.xml;
    requires static java.net.http;
    requires io.vidocq.chappe.api;        // M1 coupling — will become an SPI in M2

    exports io.vidocq.foy.internal.async;
    exports io.vidocq.foy.internal.boot;
    exports io.vidocq.foy.internal.bridge;
    exports io.vidocq.foy.internal.container;
    exports io.vidocq.foy.internal.dispatcher;
    exports io.vidocq.foy.internal.error;
    exports io.vidocq.foy.internal.http;
    exports io.vidocq.foy.internal.listener;
    exports io.vidocq.foy.internal.security;
    exports io.vidocq.foy.internal.session;
    exports io.vidocq.foy.internal.webxml;
}
module io.vidocq.foy.chappe {
    requires transitive io.vidocq.foy.api;
    requires io.vidocq.foy.core;
    requires io.vidocq.chappe.api;
    requires jakarta.servlet;
    requires jakarta.cdi;

    exports io.vidocq.foy.chappe;         // FoyChappeBoot
}
module io.vidocq.foy.cdi.vauban {
    requires transitive io.vidocq.foy.api;
    requires jakarta.cdi;
    requires io.vidocq.vauban.core;

    exports io.vidocq.foy.cdi.vauban;     // FoyVaubanBootstrap
}
module io.vidocq.foy.processor {          // build time only
    requires java.compiler;

    exports io.vidocq.foy.processor;
    provides javax.annotation.processing.Processor
            with io.vidocq.foy.processor.FoyWebComponentProcessor;
}

foy-core additionally declares uses io.vidocq.foy.spi.gen.WebComponent.

The io.vidocq.foy.internal.* packages are exported to allow advanced integrations (custom dispatcher, observability). They are not semver-stable — see Migration for variations between milestones.

Build-time code generation

  • foy-processor is a build-time annotation processor with a single requirement, java.compiler. It generates X$$FoyComponent for @WebServlet, @WebFilter, @WebListener, for unannotated Servlet / Filter / listener classes and for ServletContainerInitializer (with its @HandlesTypes), carries @MultipartConfig, @ServletSecurity, @DeclareRoles and @RunAs metadata, and writes META-INF/services/io.vidocq.foy.spi.gen.WebComponent plus META-INF/foy/class-index.list. Spec misuse is a compile error; a class that cannot be generated is reported as a note.

  • FoyWebExtension (in foy-cdi-vauban) is a CDI Build Compatible Extension. It runs inside vauban-processor, which is itself build-time only: unscoped web components become @Dependent, duplicate servlet or filter names fail the build, and a synthetic @Singleton io.vidocq.foy.spi.cdi.CdiWebComponents bean lists the components for WebAppDiscovery, which merges the beans of the application and of every library archive. A web-annotated Servlet, Filter or EventListener bean missing from every index (its archive was built without foy-cdi-vauban on the processor path) is still discovered, with one WARNING per class.

  • See Component resolution tiers for the runtime side and Usage for the setup.

Public API: FoyChappeBoot

Method Description

FoyChappeBoot.builder()

Returns a fresh Builder.

Builder.beanManager(BeanManager bm)

Required. The BeanManager of the CDI container — Vauban’s, Weld’s, any CDI 4 container’s — from which @WebServlet / @WebFilter / @WebListener beans will be resolved. NEW With it, Foy also activates the CDI request context for each request; see Other CDI containers NEW.

Builder.contextPath(String path)

URL prefix served by this container. Default: "/".

Builder.sessionTimeoutSeconds(int seconds)

Session timeout in seconds, rounded up to whole minutes; a web.xml <session-timeout> wins. Default: 1800 (30 min).

Builder.classLoader(ClassLoader cl)

Class loader used for resources and class resolution during deployment.

Builder.webXml(InputStream in)

Optional web.xml stream, merged with the annotated components per Servlet 6.1 §8.2.3.

Builder.build()

Deploys the web application through WebAppDeployer. Returns Optional<Mounted>. Empty if no Servlet/Filter/Listener bean was discovered.

Mounted.handler()

The io.vidocq.chappe.api.Handler to mount through a Chappe Router.Builder.mount(mountPrefix, handler), or to pass directly to Server.builder().handler(…​) when the contextPath is /.

Mounted.mountPrefix()

Mount prefix to use ("" if contextPath == "/").

Mounted.servletContext()

The container’s VidocqServletContext — useful for observability.

Mounted.close()

Destroys servlets and filters and fires ServletContextListener.contextDestroyed. contextInitialized already fired during build().

Public SPI (foy-api)

Type Role

io.vidocq.foy.spi.session.SessionStore

Session storage backend. Default: InMemorySessionStore. Implement to back Redis, Hazelcast, JDBC, etc. Optional default methods: rename(oldId, session) (used by changeSessionId; default put-then-remove) and sessions() (the snapshot scanned by the expiry reaper and invalidated on undeploy; default empty, for stores that expire entries themselves; with the default, sessions expired by the store or left at undeploy fire no sessionDestroyed, valueUnbound or attributeRemoved).

io.vidocq.foy.spi.security.SecurityProvider

Application authentication. Default: AnonymousSecurityProvider (dev) or BasicAuthenticator. Implement for OIDC, LDAP, JWT.

io.vidocq.foy.spi.security.AuthenticatedUser

Immutable authenticated-user representation (login, roles, attributes).

web.xml and web-fragment.xml coverage

WebXmlParser reads the Servlet 6.1 web-app_6_1 schema and the web-fragment variant. The root element is checked, DOCTYPE is disallowed, and a parse error names the offending element. Descriptors are merged with the annotated components per Servlet 6.1 §8.2.3: web.xml wins on a same-name conflict, a conflict between two fragments fails the deployment, a url-pattern claimed by two components fails (§12.2), and metadata-complete="true" ignores annotations and fragments. <init-param> duplicates keep the first value; <async-supported> is tri-state (unset, true, false); <session-config>/<cookie-config> merges per sub-element.

Element Status Notes

<display-name>, <context-param>, <listener>, <error-page>, <locale-encoding-mapping-list>

applied

<servlet>, <servlet-mapping> (init-param, load-on-startup, async-supported, enabled, multipart-config)

applied

<enabled>false</enabled> leaves the servlet unserved; web.xml multipart-config wins over @MultipartConfig

<servlet> with <jsp-file>

skipped

no JSP engine: logged at WARNING, the rest of the deployment proceeds

<run-as>

parsed

not applied

<filter>, <filter-mapping> (dispatcher, servlet-name, url-pattern)

applied

async-supported is tri-state

<session-config> (session-timeout, cookie-config, tracking-mode)

applied

the session cookie follows SessionCookieConfig; tracking modes reach getEffectiveSessionTrackingModes() and govern input as well as output; a session-timeout of zero or less never expires

<mime-mapping>

applied

feeds ServletContext.getMimeType

<request-character-encoding>, <response-character-encoding>

applied

ServletContext getters return null when unset

<default-context-path>

applied

used when the builder sets no context path; web.xml only — a fragment’s is ignored with a warning

<welcome-file-list>

applied

consumed by the container default servlet: static file first, then servlet; see Welcome files

<deny-uncovered-http-methods>

parsed

enforced with the security phase

<security-constraint>, <login-config>, <security-role>

parsed

enforced with the security phase

<name>, <ordering>, <absolute-ordering>

applied

fragment name and §8.2.2 ordering

<jsp-config>, <resource-ref>, <env-entry> and the other Jakarta EE environment elements

ignored

no JSP engine and no naming environment in Foy

The version and metadata-complete attributes of <web-app> and <web-fragment> are honoured. A DOCTYPE (Servlet 2.2 and 2.3 descriptors) is accepted without being resolved: the public id sets the version, a version before 2.5 implies metadata-complete, and an entity reference is rejected (see Legacy deployment descriptors).

HttpServletMapping

HttpServletRequest.getHttpServletMapping() is implemented for every dispatch type; getMatchValue(), getPattern(), getServletName() and getMappingMatch() follow Servlet 6.1 §12.2:

Request MappingMatch getPattern() getMatchValue()

/a/b with servlet /a/b

EXACT

/a/b

a/b

/a/b/c with servlet /a/*

PATH

/a/*

b/c

/x/y.ts with servlet *.ts

EXTENSION

*.ts

x/y

/ with servlet ""

CONTEXT_ROOT

""

""

any other path, container default servlet or application /

DEFAULT

/

""

On a forward the mapping is that of the target; jakarta.servlet.forward.mapping keeps the mapping of the original request, and jakarta.servlet.include.mapping the one of the included target. The mapping of a nested forward stays the original one. A named dispatch sets no mapping attribute.

Dispatcher attributes

Dispatch Attributes set on the target request

FORWARD

jakarta.servlet.forward.{request_uri,context_path,servlet_path,path_info,query_string,mapping} from the original request, kept unchanged by nested forwards. Hidden from an included target.

INCLUDE

jakarta.servlet.include.{request_uri,context_path,servlet_path,path_info,query_string,mapping} of the included resource, removed when the include returns. A forward from inside an include hides them.

ERROR

jakarta.servlet.error.{status_code,message,exception_type,exception,request_uri,servlet_name,query_string}; getDispatcherType() is ERROR.

ASYNC

jakarta.servlet.async.{request_uri,context_path,servlet_path,path_info,query_string,mapping}.

Session configuration

Setting Behaviour

Timeout

<session-timeout> (minutes), ServletContext.setSessionTimeout (also from a ServletContainerInitializer) or FoyChappeBoot.Builder.sessionTimeoutSeconds; zero or less never expires. A virtual-thread reaper runs every min(60 s, max(1 s, timeout / 2)) and fires sessionDestroyed; any request carrying a session id accesses (and so extends) the session.

getLastAccessedTime()

Time of the previous request.

Tracking modes

Default {COOKIE, URL}; SSL only when configured. Enforced on input: the cookie is read only with COOKIE, ;jsessionid only with URL and when no cookie is sent; the cookie wins when both are present.

changeSessionId()

Keeps the attributes, fires HttpSessionIdListener.sessionIdChanged, sends the new cookie; the requested id is updated.

Cookie

Built from SessionCookieConfig (name, path, domain, HttpOnly, Secure, Max-Age, attributes). On a secure request the cookie is Secure unless setSecure or <secure> was set. For the root context the cookie path is /.

URL rewriting

encodeURL, encodeRedirectURL append ;jsessionid=<id> to application URLs while the client sends no cookie and URL is a tracking mode.

Request identity and connection

getRequestId() is unique per request (a JVM-wide counter). getServletConnection() reports the protocol, the isSecure() flag, and a connection id derived from the socket address pair (remote-ip:port-local-ip:port, ?-? when Chappe gives no address). Two limits come from the Chappe Request API, which exposes neither a connection identity nor the HTTP/2 stream id (BUG-20261009-09, open, a Chappe follow-up):

  • the connection id is not unique for the lifetime of the JVM: two connections reusing the same ephemeral port get the same id;

  • getProtocolRequestId() is "" for HTTP/2 instead of the stream id, and getProtocolConnectionId() is "" on every protocol.

ServletContext.getVirtualServerName() returns vidocq unless FoyChappeBoot.Builder.virtualServerName(…​) sets it. getRealPath(path) answers the file-system path of a resource held in a directory (canonicalised first), and null for a resource inside a jar or a missing one.

Servlet 6.1 API comparison

Area Foy status Notes

Core (Servlet, HttpServlet, Filter)

✅

Near-complete coverage in the official TCK.

RequestDispatcher (forward / include / named / error / async)

✅

Parameter merge, named dispatch of unmapped servlets, nested forward keeping the originals, response closed after a forward. Cross-context (getContext) implemented via CrossContextRegistry (contextPath match on contexts deployed in the same JVM).

Default servlet, welcome files, HttpServletMapping

✅

Static content with conditional GET and ranges, welcome files, MappingMatch for every dispatch; see Usage.

Web fragments (web-fragment.xml)

✅

Implemented — discovery, §8.2.2 ordering and §8.2.3 merge; see web.xml and web-fragment.xml coverage.

Async (AsyncContext)

✅

Cycles, AsyncListener events (onStartAsync, onTimeout, onError, onComplete), timeout routed to an error dispatch (500, error pages applied); see Usage.

Response streaming and buffering

✅

Commit on buffer overflow, flushBuffer and a complete Content-Length; live chunked (HTTP/1.1) or DATA-frame (HTTP/2) body through a bounded pipe; setBufferSize honoured.

Non-blocking I/O (ReadListener, WriteListener)

✅

On async-started requests and upgraded connections. Not resumed after an AsyncContext.dispatch; no ReadListener.onError on async timeout.

Response trailers (setTrailerFields)

✅

HTTP/1.1 chunked and HTTP/2; supplier called after the body on Chappe’s thread; see Usage for the limits.

HTTP Upgrade (HttpUpgradeHandler, WebConnection)

✅

HTTP/1.1 only; blocking and non-blocking streams; destroy() on close, I/O error or undeploy; no idle timeout once upgraded.

HTTP/2 server push (PushBuilder)

❌

Not supported: newPushBuilder() returns null (deprecated in Servlet 6.1).

Multipart (@MultipartConfig)

⚠️

Buffered in memory — no streaming parse, no temp-file spill; limit/threshold handling partial (8 of 14 PartTests/Part1Tests still fail).

Sessions

✅

InMemorySessionStore; SPI SessionStore for distribution; changeSessionId, expiry reaper, tracking modes, URL rewriting.

Security (Basic)

⚠️

BasicAuthenticator + @ServletSecurity constraints (SecurityConstraintEnforcer); <security-constraint> in web.xml and the fragments is parsed and merged but not enforced yet (security phase, Phase 6). 8 of 14 secbasic TCK tests still fail.

Security (Form, Digest, Jakarta Auth)

❌

Not implemented.

WebSocket (Jakarta WebSocket over the upgrade)

❌

Not implemented (planned, no date); HTTP Upgrade itself is available.

JSP

❌

Not supported by Foy itself; planned through a JSP-engine SPI plugged by Ibarra (Jakarta Pages 4.0).

Other CDI containers NEW

Foy does not need Vauban: give FoyChappeBoot the BeanManager of any CDI 4 container (CDI.current().getBeanManager()). Without foy-cdi-vauban’s build-time index, Foy finds the web components by walking the container’s `Servlet, Filter and EventListener beans (foy#18).

  • The request context is active for each request — Foy activates it through the standard RequestContextController before the application’s request listeners, and deactivates it after them, on the thread that served the request (an asynchronous request included: Foy waits for it). @RequestScoped beans work in servlets, filters and listeners, and the container fires @Initialized(RequestScoped.class) and @Destroyed(RequestScoped.class). If the context is already active (an embedding runtime activated it), Foy leaves it alone. Up to 0.3.0, Foy activated it under no container, Vauban included (BUG-20261010-01). On Vauban, this needs a Vauban with its built-in RequestContextController (vauban#147); before it, the request context was not activated under Vauban and a @RequestScoped bean failed with ContextNotActiveException.

  • NEW The session context, on Vauban — @SessionScoped beans live in the HttpSession: one instance per session, created on first use (which creates the session), destroyed with it when it is invalidated or expires, between @BeforeDestroyed and @Destroyed(SessionScoped.class); a new session fires @Initialized(SessionScoped.class). CDI Lite leaves the session context to the servlet container (CDI 4.1 §6.7.2): Vauban’s vauban-webcontexts provides it, and foy-cdi-vauban, which depends on it, drives it from Foy’s request and session events (foy#21). Nothing to configure. Passivation is not covered: the instances are not written out with the session.

  • The session context, on another container — Foy finds the listeners that drive a CDI implementation’s contexts through the io.vidocq.foy.spi.cdi.CdiContextListeners service, registers them ahead of the application’s listeners when it is given that implementation’s BeanManager, and an adapter such as foy-cdi-vauban provides them. Unlike a ServletContainerInitializer, such a provider does not make Foy start an application that has no web component. Weld’s own servlet integration, weld-servlet-core, also runs on Foy: its ServletContainerInitializer starts Weld and manages the request and session contexts itself; Foy is then given no BeanManager, and servlets declared in web.xml reach their beans through CDI.current().

foy-it-weld, part of every build and never published, runs Foy on Chappe with Weld SE 6.0 (CDI 4.1) on a class path, without foy-cdi-vauban: a servlet, a filter and a listener get an injected bean, a @RequestScoped bean is one instance per request, and the request context events fire. No Open Liberty module: on a server, use the server’s own Servlet container.

NEW The session scope has its own pair of modules, never published, running the same HTTP scenarios from foy-it-session-scenarios: foy-it-weld-servlet, the reference, where Weld’s servlet integration manages the session context, and foy-it-vauban-session, where vauban-webcontexts does. Both check one cart per session across requests, two carts for two clients, a new and empty cart after invalidate() with the old one’s @PreDestroy run, and the session events.

Compatibility

  • Java 25 (LTS)

  • Maven 3.9.16

  • Jakarta Servlet 6.1

  • Any CDI 4 container (Vauban, Weld) — see Other CDI containers NEW

  • Strict Java Modules, virtual threads (a Foy VT per request, next to Chappe’s connection VT)

  • AOT-friendly (no reflection, no dynamic proxy)

Bugs and benchmarks

  • BUG.md — reproducible bugs, hypotheses, status.

  • TCK.md — full TCK measurement report.

  • Benchmarks: no numbers recorded yet — a BENCH.md will be created with the first measured run (workspace convention).