Handling web requests
Expose an HTTP endpoint by annotating a handler method. The return value becomes its response body, so you can use the same application code and testing tools as for other messages.
For incoming provider callbacks, follow Handle external callbacks.
Define an endpoint
Section titled “Define an endpoint”@HandleGet("/hello")String hello() { return "Hello, world!"; }@HandleGet("/hello")fun hello() = "Hello, world!"Register the handler with your application, or use Spring’s @Component. A GET /hello request then returns
Hello, world!. See Hello world for the local setup.
A @HandleGet without a mapping value or @Path serves /. Empty request URLs also resolve to this root;
automatic HEAD and OPTIONS use the same route. This applies in Java and Kotlin. When documented with @ApiDoc,
this endpoint also appears under / in compiled and runtime OpenAPI. Absolute origins without a path use /
as well, with the origin retained as the operation server.
Multiple paths or methods
Section titled “Multiple paths or methods”Use the general-purpose @HandleWeb annotation if you want to support multiple paths, multiple methods, or define a custom method:
@HandleWeb(value = "/users/{userId}", method = {"GET", "DELETE"})public CompletableFuture<?> handleUserRequest(WebRequest request, @PathParam String userId) { return switch (request.getMethod()) { case "GET" -> Fluxzero.query(new GetUser(userId)); case "DELETE" -> Fluxzero.sendCommand(new DeleteUser(userId)); default -> throw new UnsupportedOperationException(); };}@HandleWeb(value = ["/users/{userId}"], method = ["GET", "DELETE"])fun handleUserRequest(request: WebRequest, @PathParam userId: String): CompletableFuture<*> { return when (request.method) { "GET" -> Fluxzero.query(GetUser(userId)) "DELETE" -> Fluxzero.sendCommand(DeleteUser(userId)) else -> throw UnsupportedOperationException() }}@HandleWeb(value = "/users/{userId}", method = HttpRequestMethod.ANY)public CompletableFuture<?> handleAllUserMethods(WebRequest request, @PathParam String userId) { // Handle any method (GET, POST, DELETE, etc.) ...}@HandleWeb(value = ["/users/{userId}"], method = HttpRequestMethod.ANY)fun handleAllUserMethods(request: WebRequest, @PathParam userId: String): CompletableFuture<*> { // Handle any method (GET, POST, DELETE, etc.) ...}Suppressing a response
Section titled “Suppressing a response”If you want to listen to a WebRequest without returning a response—e.g. for monitoring or audit logging—use passive = true:
@HandlePost(value = "/log", passive = true)public void log(WebRequest request) { logService.store(request);}@HandlePost(value = "/log", passive = true)fun log(request: WebRequest) { logService.store(request)}Explicit response headers
Section titled “Explicit response headers”For explicit response headers, return a WebResponse. Its contentType(...) builder method replaces any inferred or
previously configured content type, so .payload(script).contentType("text/javascript") produces one JavaScript
content type in both Java and Kotlin. The generic header(...) method still appends values for multi-valued headers
such as Set-Cookie.
Graph responses
Section titled “Graph responses”A handler may return a Graph<T> as composed JSON, including selected child paths and derived properties.
Use @FilterContent when the response needs viewer-specific filtering. See
User-based content filtering.
Dynamic path parameters
Section titled “Dynamic path parameters”Use @PathParam to extract dynamic segments from the URI:
@HandleGet("/users/{id}")public UserAccount getUser(@PathParam String id) { return userService.get(id);}@HandleGet("/users/{id}")fun getUser(@PathParam id: String): UserAccount { return userService.get(id)}If no name is provided to @PathParam, the parameter name is used by default.
Path parameters are percent-decoded once as UTF-8 after route matching and before type conversion.
For example, order%3A1 becomes order:1; a literal + stays +. Encoded slashes (%2F)
belong to the parameter value and do not introduce route segments. Do not decode the injected value again.
Other parameter annotations
Section titled “Other parameter annotations”You can also use these to extract other parts of the request:
@QueryParam– extract query string values@HeaderParam– extract headers@CookieParam– extract cookies@FormParam– extract form-encoded values (e.g., from POST body)
Named body parameters that are missing or JSON null may fall back to another configured parameter source.
See WebRequestContext for source precedence and conversion.
URI prefixing and composition with @Path
Section titled “URI prefixing and composition with @Path”Use @Path on packages, classes, methods, and properties to construct URI paths. Paths are composed from outermost to innermost.
- A path starting with
/resets the chain - Empty paths use the simple name of the enclosing package or class
- If applied to a property (field or getter), its value is used as a dynamic segment
@Pathpackage my.example.api;
@Path("users")public class UserHandler {
@Path("{id}") @HandleGet public User getUser(@PathParam String id) { ... }}This matches /api/users/{id}. If @Path("/users") had been used on the class instead, it would reset the prefix and match /users/{id}.
Serving static files
Section titled “Serving static files”Use @ServeStatic to serve static content directly from your app without an external web server:
@ServeStatic(value = "/web", resourcePath = "/static")public class WebAssets {}This serves files from /static (in classpath or filesystem) under the URI path /web/**.
Static responses use content-based ETags, so changed bytes invalidate a cached response even if file size and
modification time are unchanged. On conditional GET/HEAD requests, If-None-Match takes precedence over
If-Modified-Since. The validator identifies the selected uncompressed, gzip, or Brotli file.
For immutable resources in regular JARs opened by the handler, each entry’s digest is computed once and reused
until that archive is closed. Reopening the archive computes fresh digests; compressed entries are cached separately.
The cache retains entry paths and digests, not file contents. Filesystem overrides, exploded classpaths and externally
managed nested JAR resources are still read with bounded memory on every request. Equal size and modification time
do not prove equal contents. Response bodies remain streamed.
Keep JARs immutable while open and mutable files stable until pending responses finish. Use fingerprinted URLs with
suitable cache lifetimes for large or frequently requested assets.
Features
Section titled “Features”- Supports classpath and file system resources
- Optional fallback file (e.g. for SPAs)
- Clean URLs for statically exported routes
- Smart
Cache-Controlheaders - Conditional requests using ETags and modification dates
- GZIP and Brotli compression (if precompressed assets are present)
Full annotation example
Section titled “Full annotation example”@ServeStatic( value = "/assets", resourcePath = "/public", fallbackFile = "index.html", cleanUrls = true, immutableCandidateExtensions = {"js", "css", "svg"}, maxAgeSeconds = 86400)@ServeStatic( value = ["/assets"], resourcePath = "/public", fallbackFile = "index.html", cleanUrls = true, immutableCandidateExtensions = ["js", "css", "svg"], maxAgeSeconds = 86400)Parameters:
Section titled “Parameters:”value: Web URI path(s) to exposeresourcePath: Filesystem or classpath rootfallbackFile: Fallback for unknown paths (e.g.index.html)cleanUrls: Whether extensionless paths first try<path>.htmland<path>/index.htmlbefore the fallbackimmutableCandidateExtensions: Enables long-lived caching for fingerprinted assetsmaxAgeSeconds: Cache duration for other resources
Example: serving a React app
Section titled “Example: serving a React app”@ServeStatic(value = "/app", resourcePath = "/static", fallbackFile = "index.html")public class WebFrontend {}@ServeStatic(value = ["/app"], resourcePath = "/static", fallbackFile = "index.html")class WebFrontendThis serves:
/app/index.html,/app/main.js, etc. from/static/app/onboardingfrom/static/onboarding.htmlor/static/onboarding/index.htmlwhen clean URLs are enabled- Falls back to
index.htmlfor client-side routing
Set cleanUrls = false if extensionless paths should only try the exact path before the fallback.
To restrict resolution:
- Use
classpath:/...to only serve classpath files - Use
file:/...to only serve local files
Combining static and dynamic handlers
Section titled “Combining static and dynamic handlers”You can mix @ServeStatic with dynamic handlers:
@Path("/app")@ServeStatic("static")public class AppController {
@HandleGet("/status") public Status getStatus() { return new Status("OK", Instant.now()); }
@HandlePost("/submit") public SubmissionResult submitForm(FormData data) { return formService.handle(data); }}@Path("/app")@ServeStatic("static")class AppController { @HandleGet("/status") fun getStatus(): Status = Status("OK", Instant.now())
@HandlePost("/submit") fun submitForm(data: FormData): SubmissionResult = formService.handle(data)}This will:
- Serve
/app/static/**from thestatic/directory - Also handle
/app/statusand/app/submitdynamically
Why this design?
Section titled “Why this design?”The Web Gateway receives public HTTP(S) and WebSocket traffic and forwards it through the Runtime as messages. Your application handles those messages without exposing its own public HTTP server. Requests and responses share the tracing and audit facilities used by commands and queries.
Applications control consumption through their trackers. Passive handlers can observe requests without producing another response. You still need the appropriate authentication and authorization on exposed operations.
Delivery and proxy configuration
Section titled “Delivery and proxy configuration”HTTP requests use the same tracked processing model as other messages. A storage acknowledgement does not mean the handler has finished, and retries after a failure can repeat work. Keep external side effects idempotent; see Delivery defaults and completion.
Proxy response header buffers
Section titled “Proxy response header buffers”Most applications can use the default proxy settings. For measured header-buffer or memory pressure, the standalone and embedded proxy expose these startup properties:
| Property | Environment variable | Purpose |
|---|---|---|
fluxzero.proxy.responseHeaderBufferSize | FLUXZERO_PROXY_RESPONSE_HEADER_BUFFER_SIZE | Initial response-header buffer size in bytes; normally 8192. |
fluxzero.proxy.http2MaxResponseHeaderSize | FLUXZERO_PROXY_HTTP2_MAX_RESPONSE_HEADER_SIZE | HTTP/2 response-header limit in bytes; defaults to 16384. |
fluxzero.proxy.useOutputDirectByteBuffers | FLUXZERO_PROXY_USE_OUTPUT_DIRECT_BYTE_BUFFERS | Use direct output buffers (true) or Java heap buffers (false). |
These tune buffer allocation, not response-body limits or the overall process-memory budget. Restart after changing them and measure the effect on your workload. The initial capacity must be positive and no greater than the effective response-header maximum. A capacity below that maximum can trigger Jetty’s known HTTP/1.1 header-growth limitation; matching it avoids that path at the cost of larger buffers. See ProxyServer for the settings.
The HTTP/2 limit counts the complete uncompressed header list, including per-field overhead. It defaults to 16 KiB regardless of defaults version; choose a larger explicit value when required. Oversized headers fail rather than being truncated, and Jetty may close the connection, affecting other streams. The initial buffer setting applies to HTTP/1; request-header limits are unchanged.
Error responses and diagnostics
Section titled “Error responses and diagnostics”The default response mapper preserves explicit FunctionalException messages, including application-defined
validation and authorization feedback. Treat these messages as public. SDK-generated authentication and role-denial
messages omit the user ID and handler name. The same rule applies to automatic WebSocket handshakes.
If a request matches a route but its JSON body is malformed or contains an unreadable value, such as an invalid
date, the endpoint is not invoked. Deserialization failures return HTTP 400 with a fixed request-format message,
including when a UserProvider is configured. Unexpected technical failures return HTTP 500 with a fixed message. The original exceptions and causes remain available internally for diagnosis.
Explicit WebResponse objects and custom response mappers remain under application control. These rules apply to
both Java and Kotlin handlers.
Validation paths, including collection indexes and map keys, remain available for identifying the failing field. Choose suitable public field keys and validation messages when exposing structured validation results. Internal error reporting still retains useful causes, stack traces and correlation context; it is not a general-purpose redactor for application-written exception messages or logs.
Console error reporting limits automatic web request metadata to the method and bounded path, omitting headers, session data, origin, query and fragment. Correlation data and explicit application diagnostics remain available.
© 2026 Fluxzero