Skip to content

Sending web requests

Use the web gateway to call an HTTP API and read its response. Requests share Fluxzero’s tracing, audit and testing facilities. A named command or query keeps the external operation recognizable to the rest of your application.

For the complete application pattern, including tests and work after a Model commit, see Building API integrations.

Give each external interaction a local command or query

Section titled “Give each external interaction a local command or query”

Use a named command for an external action and a typed query (Request<T>) for an external read. Put the actual WebRequestGateway call in the payload’s @HandleCommand or @HandleQuery method. Other handlers invoke the operation through Fluxzero.sendCommandAndWait(...) or Fluxzero.queryAndWait(...), just like other application behavior. Choose command versus query by the operation’s meaning, not only its HTTP verb.

The handler below runs locally without registration. The example partner API returns an order as JSON with HTTP 200. PartnerOrder is the response value; partner.api.token (PARTNER_API_TOKEN) supplies the credential.

public record GetPartnerOrder(@NotNull UUID orderId) implements Request<PartnerOrder> {
@HandleQuery
PartnerOrder handle() {
var request = WebRequest.get("https://partner.example/api/orders/" + orderId)
.header("Authorization", "Bearer " + ApplicationProperties.requireProperty("partner.api.token"))
.build();
var response = Fluxzero.sendWebRequestAndWait(request);
if (response.getStatus() != 200) {
throw new IllegalStateException("Order lookup returned HTTP " + response.getStatus());
}
return response.getPayloadAs(PartnerOrder.class);
}
}
public record PartnerOrder(UUID orderId, String status) {}

Call it from application code in an active Fluxzero context:

PartnerOrder existing = Fluxzero.queryAndWait(new GetPartnerOrder(orderId));

Shared URL construction, headers, settings and response mapping may live in a small helper, interface or base class. Keep each operation recognizable as its own command/query; do not replace them with a generic HTTP-command envelope or require callers to inject an API service. Resolve configuration through ApplicationProperties at this integration boundary. If settings need parsing, a typed settings value can be loaded here without making it a bean. The fixed example URLs stand in for a configured, validated endpoint; validate configured base URLs before appending operation paths.

A local command/query and its outgoing HTTP request have separate delivery rules. Local self-handling does not select native HTTP: the nested WebRequest still uses the auditable proxy route and configured transport retries. The local command/query itself is not a persisted job and has no independent tracker retry. Add @TrackSelf only when that operation itself must arrive through the Runtime, have a durable consumer position, or be replayed/retried independently. Add @Consumer only for a required tracking configuration. Neither is needed merely to call an API.

For an external write that depends on committed Model state, a registered post-commit event handler can dispatch the local command and wait for its outcome. Let failures reach that caller’s error/retry policy. Local dispatch does not make HTTP part of the Model transaction: do not perform external I/O in @Apply or send the write from a mutation handler while its changes are still pending. Read-only external queries may run from ordinary query/orchestration handlers; their results are not replayable Model state until explicitly recorded.


Use response.getPayloadAs(MyReply.class) in Java or response.getPayloadAs(MyReply::class.java) in Kotlin to decode a typed response. The SDK handles gzip decoding on the message-gateway route. If you consume the body as a stream, read it only once. See WebResponse for body conversion and stream ownership.

Fluxzero Auditlog automatically replaces standard credential-header values such as Authorization, Proxy-Authorization, X-Api-Key, Cookie and Set-Cookie with <value scrambled> in visible web request/response records. Matching is case-insensitive, and downloads through Auditlog apply the same filtering. Requests remain auditable without exposing these bearer tokens or API keys in the visible logs. Using such a header is not a reason to replace the gateway or opt into native execution.

The example resolves the credential from partner.api.token through ApplicationProperties; its conventional environment variable is PARTNER_API_TOKEN. Keep it in runtime configuration instead of source code.

This is Auditlog projection/response masking: the original credential still reaches the HTTP transport. It does not promise removal of secrets from arbitrary URLs, bodies, custom fields or application-written logs.


You can send requests asynchronously:

Fluxzero.get().webRequestGateway()
.send(request)
.thenAccept(response -> log.info("Received: {}", response.getBodyString()));

Or send them fire-and-forget:

Fluxzero.get().webRequestGateway()
.sendAndForget(Guarantee.STORED, request);

Flux distinguishes between relative and absolute URLs:

  • Absolute URLs (e.g., https://api.example.com/...) → Sent via the Flux Web Proxy by default; explicit native settings execute directly from the application
  • Relative URLs (e.g., /internal/task) → Routed to local handlers in other connected Flux apps

This enables request-response flows across distributed services without coupling.


Timeouts, retries and an explicit native alternative

Section titled “Timeouts, retries and an explicit native alternative”

Waiting for the response is independent of transport: sendAndWait blocks the caller, while send returns a future. In the local command/query handler, pass WebRequestSettings to either method (or to Fluxzero.sendWebRequestAndWait) to configure the request:

WebRequestSettings settings = WebRequestSettings.builder()
.timeout(Duration.ofSeconds(5))
.maxRetries(2)
.retryDelay(Duration.ofMillis(250))
.retryableStatusCodes(Set.of(502, 503, 504))
.build();
WebResponse response = Fluxzero.sendWebRequestAndWait(request, settings);

maxRetries counts additional attempts and defaults to zero. Transport failures and selected statuses can trigger retries; all attempts and retryDelay share the overall timeout. Inspect the returned status before decoding the body. Retry writes only when their semantics or an external idempotency contract make repetition safe. Domain polling and reconciliation are separate from these transport retries.

Buffered request bodies retain their serialized byte length in both the proxy and native transports. For HTTP/1.1, the client supplies Content-Length automatically; do not add it manually. An endpoint that requires a known body length can use the ordinary proxy route, including its WebRequest/WebResponse audit trail.

Use .useNativeHttpClient(true) only for a deliberate requirement to execute an absolute HTTP(S) request directly from the application. It still uses the SDK gateway and its timeout/retry configuration, but bypasses WebRequest and WebResponse message logging, local web handlers, dispatch interceptors and consumer isolation. This trades away the normal message audit route and is not necessary merely because a request contains authentication headers.

TestFixture still handles native-configured requests through registered remote stubs and preserves retry counts and retryable statuses without real retry delays. This preserves the gateway test boundary; it does not qualify real network or proxy behavior.


The consumer field in WebRequestSettings configures outbound HTTP processing on the proxy route. It is separate from @Consumer on a command/query handler and does not require making the calling self-handler tracked. Use it when you need to isolate HTTP traffic:

WebRequestSettings settings = WebRequestSettings.builder()
.consumer("external-api-xyz")
.timeout(Duration.ofSeconds(5))
.build();

On the default proxy route, this allows you to:

  • Isolate third-party API traffic (e.g., apply rate limits)
  • Use different retry or error-handling strategies
  • Segment outbound traffic by destination

Register a handler for the external URL in the fixture. Then dispatch the actual query: its own handler builds the request and decodes the stubbed response. This checks your integration boundary without a network call.

@Consumer(name = "partner-api-stub")
static class PartnerApiStub {
@HandleGet("https://partner.example/api/orders/{orderId}")
WebResponse getOrder(@PathParam("orderId") UUID orderId) {
return WebResponse.builder().status(200).contentType("application/json")
.payload(Map.of("orderId", orderId, "status", "accepted"))
.build();
}
}
@Test
void readPartnerOrder() {
var orderId = UUID.fromString("00000000-0000-0000-0000-000000000001");
TestFixture.create(new PartnerApiStub())
.withProperty("partner.api.token", "test-token")
.whenQuery(new GetPartnerOrder(orderId))
.expectResult(new PartnerOrder(orderId, "accepted"))
.expectOnlyWebRequests(WebRequest.get("https://partner.example/api/orders/" + orderId)
.header("Authorization", "Bearer test-token").build());
}

The stub’s @Consumer gives the simulated remote endpoint independent tracked processing, including in asynchronous fixtures where the caller waits for its response. It belongs to the test endpoint, not to the production local command/query. Use TestFixture.createAsync(new PartnerApiStub()) in Java or TestFixture.createAsync(PartnerApiStub()) in Kotlin when the scenario needs asynchronous tracking. Local self-handlers still execute locally in that fixture.

This checks the application’s command/query behavior, request construction, URL routing and response mapping without real network access. It does not prove DNS, TLS, proxy behavior or the third party’s deployed contract. Add cases for non-2xx responses, malformed payloads and relevant timeout/retry outcomes. Preserve unexpected technical failures unless the application’s contract explicitly maps them to an expected business outcome.


  • ✅ Give each external interaction a local command/query with its WebRequestGateway call in the handler
  • ✅ Default routing follows the URL (relative → internal, absolute → proxy); native execution is explicit
  • ✅ Default proxy traffic is auditable, with standard credentials masked in visible Auditlog records
  • ✅ Supports retries, timeouts, and consumer-based isolation
  • ✅ Easily mock remote endpoints in tests

Isolate outbound failures without changing existing configurations

Section titled “Isolate outbound failures without changing existing configurations”

The additional resilience controls are opt-in on both supported SDK lines. Existing applications retain retryPolicy(LEGACY), fixed retry delays and no per-origin concurrency limit; no defaults-version threshold changes these settings.

Use retryPolicy(RetryPolicy.SAFE) for configured retries of GET, HEAD, PUT, DELETE, OPTIONS and TRACE, or of a request with exactly one nonblank Idempotency-Key header. For POST/PATCH, use a stable key supported by the upstream’s deduplication contract. The SDK reuses the same serialized body and headers across attempts; the presence of a key alone cannot guarantee that an upstream deduplicates it. RetryPolicy.IDEMPOTENT is an explicit application guarantee for operations with proven idempotent semantics. LEGACY preserves the previous method-independent retry behavior. None of these choices enables retries unless maxRetries is positive.

WebRequestSettings settings = WebRequestSettings.builder()
.timeout(Duration.ofSeconds(5)).maxRetries(2)
.retryPolicy(RetryPolicy.SAFE)
.retryDelay(Duration.ofMillis(250)).retryJitter(true)
.build();

With jitter enabled, each retry waits a randomized duration between half and all of retryDelay. All attempts, redirects and retry waits still consume the same timeout budget; retries do not reset it. Fixture retries enforce the same safety policy without actual retry waits. A new SDK publishes maxRetries=0 for disallowed SAFE retries, so an older proxy also refrains from retrying them. Jitter requires an updated proxy on the proxy route.

Configure concurrency limits through the normal property infrastructure:

Transport ownerPropertyEnvironment variableDefault
Proxy processfluxzero.proxy.forward.maxConcurrentPerOriginFLUXZERO_PROXY_FORWARD_MAX_CONCURRENT_PER_ORIGIN0 (disabled)
Native application clientfluxzero.web.native.maxConcurrentPerOriginFLUXZERO_WEB_NATIVE_MAX_CONCURRENT_PER_ORIGIN0 (disabled)

A positive value limits outstanding executions per original HTTP origin: lowercase scheme and hostname plus the effective port, without DNS resolution. Paths, credentials and query strings do not create separate origins. Limits are local to the owning client/process, not cluster-wide. Namespace views of one native client share its limit. Idle origins are removed from bookkeeping; no permanent hostname cache or additional unbounded queue is kept.

The proxy admits requests within its existing bounded queue and selects eligible segment heads before reserving a global execution slot. Set the per-origin limit below the existing global concurrency limit to leave execution capacity for other origins. Requests in one segment remain ordered, including response publication. The global outstanding bound and backpressure remain in force: a completely full queue or a preceding request in the same segment can still delay another origin. The limit is held across retries and response publication and is released on completion or shutdown handoff. Waiting in the proxy queue consumes the existing request deadline.

Native execution has no added waiting queue: exceeding its limit returns HTTP 503 immediately, without an HTTP attempt or automatic transport retry of that local response. The permit is held across retries and released on completion, failure or cancellation; closing the client cancels its limited in-flight requests and retry waits.

Redirects retain the original request’s permit and existing redirect policy. Multiple original origins redirecting to the same destination are not a shared destination bulkhead. Use SAME_ORIGIN or NEVER when that distinction matters. These local limits also do not deduplicate replayed requests or replace domain idempotency.


© 2026 Fluxzero