Skip to content

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.

@HandleGet("/hello")
String hello() { return "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.

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 = HttpRequestMethod.ANY)
public CompletableFuture<?> handleAllUserMethods(WebRequest request, @PathParam String userId) {
// Handle any method (GET, POST, DELETE, etc.)
...
}

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);
}

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.

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.

Use @PathParam to extract dynamic segments from the URI:

@HandleGet("/users/{id}")
public UserAccount getUser(@PathParam String id) {
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.

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.

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
@Path
package 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}.


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.

  • Supports classpath and file system resources
  • Optional fallback file (e.g. for SPAs)
  • Clean URLs for statically exported routes
  • Smart Cache-Control headers
  • Conditional requests using ETags and modification dates
  • GZIP and Brotli compression (if precompressed assets are present)
@ServeStatic(
value = "/assets",
resourcePath = "/public",
fallbackFile = "index.html",
cleanUrls = true,
immutableCandidateExtensions = {"js", "css", "svg"},
maxAgeSeconds = 86400
)
  • value: Web URI path(s) to expose
  • resourcePath: Filesystem or classpath root
  • fallbackFile: Fallback for unknown paths (e.g. index.html)
  • cleanUrls: Whether extensionless paths first try <path>.html and <path>/index.html before the fallback
  • immutableCandidateExtensions: Enables long-lived caching for fingerprinted assets
  • maxAgeSeconds: Cache duration for other resources
@ServeStatic(value = "/app", resourcePath = "/static", fallbackFile = "index.html")
public class WebFrontend {
}

This serves:

  • /app/index.html, /app/main.js, etc. from /static
  • /app/onboarding from /static/onboarding.html or /static/onboarding/index.html when clean URLs are enabled
  • Falls back to index.html for 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

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);
}
}

This will:

  • Serve /app/static/** from the static/ directory
  • Also handle /app/status and /app/submit dynamically

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.

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.

Most applications can use the default proxy settings. For measured header-buffer or memory pressure, the standalone and embedded proxy expose these startup properties:

PropertyEnvironment variablePurpose
fluxzero.proxy.responseHeaderBufferSizeFLUXZERO_PROXY_RESPONSE_HEADER_BUFFER_SIZEInitial response-header buffer size in bytes; normally 8192.
fluxzero.proxy.http2MaxResponseHeaderSizeFLUXZERO_PROXY_HTTP2_MAX_RESPONSE_HEADER_SIZEHTTP/2 response-header limit in bytes; defaults to 16384.
fluxzero.proxy.useOutputDirectByteBuffersFLUXZERO_PROXY_USE_OUTPUT_DIRECT_BYTE_BUFFERSUse 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.

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