HTTP Client
LuxisHttpClient is a fully async HTTP client that follows the same pattern as everything else in Luxis: one interface, two implementations, swap with one line.
| In-memory (stub) | Real HTTP (Vert.x) | |
|---|---|---|
| Factory | StubLuxisHttpClient.create(targetLuxis) | new VertxLuxisHttpClient(vertx) |
| Network | None — calls the target service in-process | Real HTTP requests |
| Speed | Instant | Network latency |
Every method returns LuxisAsync, which plugs directly into the pipeline’s asyncMap. This is important — the client is purely async, so you get the most benefit when using it inside pipelines where the framework manages the threading for you.
Using the client in a handler
Section titled “Using the client in a handler”return stream .asyncMap(ctx -> httpClient.get( "http://user-service:8080/api/users/" + ctx.in().userId, UserResponse.class)) .map(ctx -> new OrderResponse(ctx.in().body())) .complete(ctx -> HttpResult.success(ctx.in()));The asyncMap step kicks off the HTTP call without blocking the event loop. When the response arrives, the pipeline continues. The response is automatically deserialized into your type.
Building requests
Section titled “Building requests”For simple calls, use the convenience methods. The convenience overloads that take a body accept a JSON String:
httpClient.get("/api/users", UserResponse.class)httpClient.post("/api/users", requestJson, UserResponse.class)httpClient.put("/api/users/123", updatedUserJson, UserResponse.class)httpClient.delete("/api/users/123", UserResponse.class)For more control, including sending a Java object as the body (which Luxis serialises to JSON for you), use HttpClientRequest:
httpClient.post( HttpClientRequest.request("/api/users", newUser) .header("Authorization", "Bearer " + token) .queryParam("notify", "true"), UserResponse.class)Reading responses
Section titled “Reading responses”The client returns LuxisAsync<HttpClientResponse<T>, HttpErrorResponse>. Inside a pipeline’s asyncMap, the framework unwraps this for you — ctx.in() gives you the HttpClientResponse directly:
.asyncMap(ctx -> httpClient.get("/api/value", ValueResponse.class)).map(ctx -> { int status = ctx.in().statusCode(); ValueResponse body = ctx.in().body(); String contentType = ctx.in().headers().get("Content-Type"); return new MyResponse(body.result());})Outside a pipeline, resolve the future yourself:
Result<HttpErrorResponse, HttpClientResponse<String>> result = httpClient.get("/api/value").toCompletableFuture().join();Combining parallel calls
Section titled “Combining parallel calls”Sometimes a single step needs several independent async calls — and you want to fire them all at once and continue only when every one has come back. CompositeLuxisAsync combines many LuxisAsync operations (which must share the same error type) into one. It resolves to a Map<String, Result<ERR, Object>> keyed by the label you give each operation, where each value is that operation’s Result:
.<Map<String, Result<HttpErrorResponse, Object>>>asyncMap(ctx -> CompositeLuxisAsync.<HttpErrorResponse>create() .add("user", httpClient.get("/users/" + ctx.in().userId(), User.class)) .add("orders", httpClient.get("/orders/" + ctx.in().userId(), Orders.class)) .combine())All registered operations run in parallel and are always awaited — a failure of one does not stop the others. Crucially, combine() itself never decides your failure policy: every operation’s outcome is handed back to you as a Result in the map, and you choose what to do with the mix.
Collapse — fail the step if any call failed
Section titled “Collapse — fail the step if any call failed”Chain the results with flatMap/map so the first error short-circuits the whole step:
.flatMap(ctx -> { Map<String, Result<HttpErrorResponse, Object>> results = ctx.in(); return results.get("user").flatMap(user -> results.get("orders").map(orders -> new ProfileResponse((User) user, (Orders) orders)));})Accept partial results — carry on with what succeeded
Section titled “Accept partial results — carry on with what succeeded”Or fold each result and supply a fallback for the ones that failed:
.map(ctx -> { Map<String, Result<HttpErrorResponse, Object>> results = ctx.in(); User user = results.get("user").fold(err -> User.unknown(), success -> (User) success); Orders orders = results.get("orders").fold(err -> Orders.empty(), success -> (Orders) success); return new ProfileResponse(user, orders);})Because the map values are Result<ERR, Object>, you cast each success back to its known type as you read it. The labels are yours to choose; they’re the contract between the add calls and the step that consumes the results.
Per-operation timeouts
Section titled “Per-operation timeouts”The asyncMap step has a single timeout covering the whole composite — so by default one slow call holds up the entire batch until that overall timeout trips, and every result is discarded. When a call is best-effort, give it its own budget with the Duration overload of add. If it doesn’t complete in time, that entry resolves to the error you supply (just another Result.error in the map) while the rest carry on:
.<Map<String, Result<HttpErrorResponse, Object>>>asyncMap(ctx -> CompositeLuxisAsync.<HttpErrorResponse>create() // critical call — relies on the overall step timeout .add("user", httpClient.get("/users/" + ctx.in().userId(), User.class)) // best-effort enrichment — capped at 500ms .add("recommendations", httpClient.get("/recommendations/" + ctx.in().userId(), Recs.class), Duration.ofMillis(500), new HttpErrorResponse(new ErrorMessageResponse("recommendations timed out"), 504)) .combine()).map(ctx -> { Map<String, Result<HttpErrorResponse, Object>> results = ctx.in(); User user = (User) results.get("user").fold(err -> User.unknown(), s -> s); // if recommendations timed out this is the 504 error → fall back to empty Recs recs = (Recs) results.get("recommendations").fold(err -> Recs.empty(), s -> s); return new ProfileResponse(user, recs);})The underlying call isn’t cancelled — the timeout value just wins the race and any late result is ignored. Set per-operation timeouts below the step timeout so a slow non-critical call degrades gracefully instead of sinking the whole batch.
Exceptions vs. graceful errors
Section titled “Exceptions vs. graceful errors”A graceful Result.error from any operation is captured in the map for you to handle, as above. The combined async only fails (completes exceptionally) if one of its operations completes exceptionally — in which case the pipeline’s exception handler takes over, exactly as it would for a single asyncMap. The same goes for the step’s timeout: it covers the composite as a whole, so if any operation never comes back, the whole step times out (the other results are discarded and the in-flight calls are not cancelled).
Creating the client
Section titled “Creating the client”In-memory (stub)
Section titled “In-memory (stub)”Point the stub client at another TestLuxis.from(...) instance. Requests execute in-process with no network:
// The service you want to callTestLuxis<UserState> userService = TestLuxis.from(Luxis.app(UserApp::registerRoutes));
// Create a client that talks to it in-memoryLuxisHttpClient httpClient = StubLuxisHttpClient.create(userService);TestLuxis and StubLuxisHttpClient are part of luxis-test-support. See the testing guide for the dependency block.
Real HTTP (Vert.x)
Section titled “Real HTTP (Vert.x)”LuxisHttpClient httpClient = new VertxLuxisHttpClient(vertx);The handler code that uses the client doesn’t change — it just calls httpClient.get(...) either way.
Injecting the client into handlers
Section titled “Injecting the client into handlers”Pass the client to your handler’s constructor. The handler doesn’t know or care whether it’s a stub or real:
public static AppState registerRoutes(RoutesRegister routesRegister) { AppState state = new AppState();
routesRegister.jsonRoute("/orders", Method.POST, state, OrderRequest.class, new CreateOrderHandler(httpClient));
return state;}public class CreateOrderHandler implements JsonHandler<OrderRequest, OrderResponse, AppState> {
private final LuxisHttpClient httpClient;
public CreateOrderHandler(LuxisHttpClient httpClient) { this.httpClient = httpClient; }
@Override public LuxisPipeline<OrderResponse> handle(HttpStream<OrderRequest, AppState> stream) { return stream .map(this::validateOrder) .asyncMap(ctx -> httpClient.get( "http://user-service:8080/api/users/" + ctx.in().userId(), UserResponse.class)) .blockingMap(this::persistOrder) .complete(ctx -> HttpResult.success(new OrderResponse(ctx.in()))); }}In tests, wire up the stub:
TestLuxis<UserState> userService = TestLuxis.from(Luxis.app(UserApp::registerRoutes));LuxisHttpClient httpClient = StubLuxisHttpClient.create(userService);
TestLuxis<AppState> orderService = TestLuxis.from(Luxis.<AppState>app(r -> { AppState state = new AppState(); r.jsonRoute("/orders", Method.POST, state, OrderRequest.class, new CreateOrderHandler(httpClient)); return state;}));Both services run in-memory. The order service calls the user service through the stub client — no network, no Docker, milliseconds.
Configuration
Section titled “Configuration”LuxisHttpClientConfig is immutable — each setter returns a new instance, and you start from LuxisHttpClientConfig.defaults(). Use it to set a base URL, enable SSL, or enable error-aware responses:
LuxisHttpClientConfig config = LuxisHttpClientConfig.defaults() .baseUrl("http://user-service:8080");
LuxisHttpClient httpClient = StubLuxisHttpClient.create(userService, config);// orLuxisHttpClient httpClient = new VertxLuxisHttpClient(vertx, config);With a base URL configured, you can use relative paths:
httpClient.get("/api/users/123", UserResponse.class)// resolves to http://user-service:8080/api/users/123Error-aware responses
Section titled “Error-aware responses”By default, 4xx and 5xx responses are returned as successful results — you check the status code yourself. Enable errorAwareResponses to have them returned as Result.error() instead:
LuxisHttpClientConfig config = LuxisHttpClientConfig.defaults() .errorAwareResponses(true);This changes how errors flow through the pipeline. With error-aware responses enabled, a 400 from the downstream service will short-circuit your asyncMap step automatically.
WebSockets
Section titled “WebSockets”The client can also open a WebSocket connection to another service. See the WebSocket Client guide for details — the client is a mirror image of a server-side WebSocket route, with inbound and outbound flipped.
File uploads and downloads
Section titled “File uploads and downloads”Upload files with postFiles:
httpClient.postFiles( HttpClientRequest.request("/api/upload") .fileUpload("document", HttpBuffer.fromString("file contents")), UploadResponse.class)Download binary content with download:
httpClient.download(HttpClientRequest.request("/api/report"))// returns LuxisAsync<HttpClientResponse<HttpBuffer>, HttpErrorResponse>