Mastering Spring WebClient: Query Params & Path Variables
Learn how to build robust Spring WebClient requests with query parameters and path variables.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Basic knowledge of Spring Boot and dependency injection
- ✓Familiarity with reactive programming concepts (Mono/Flux)
- ✓Java 8 or later
- Use
uriBuilderfor dynamic path variables and query parameters. - Prefer
uriBuilder.queryParam()over manual string concatenation. - Always encode parameters unless you have a specific reason not to.
- Handle null parameters explicitly to avoid
NullPointerException. - For complex URIs, use
UriComponentsBuilderfor more control.
Think of WebClient as a courier service. The path variable is the specific address (like a house number) that tells the courier exactly where to go. Query parameters are like special instructions ("leave at the back door", "sign for package") that modify how the delivery is handled. You need to give both correctly or the package never arrives.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
If you've been building Spring Boot applications for any length of time, you know that making HTTP calls is a fact of life. Whether you're calling a third-party payment API, fetching user data from a microservice, or posting events to an analytics pipeline, you need a reliable HTTP client. RestTemplate was the old guard, but it's been deprecated in favor of WebClient since Spring 5.0. And for good reason: WebClient is reactive, non-blocking, and far more flexible when it comes to building complex requests.
But here's the hard truth: most teams get query parameters and path variables wrong. I've seen production outages caused by improperly encoded query strings, null pointer exceptions from missing path variables, and subtle bugs that only surface under load. In this article, I'm going to show you how to build WebClient requests the right way — with real-world examples, production debugging techniques, and the gotchas that the official docs gloss over.
We'll cover everything from the basics of uriBuilder to advanced error handling and testing. By the end, you'll be able to craft robust, maintainable HTTP calls that won't break at 3 AM.
WebClient Basics: Why You Should Care
Let's get one thing straight: if you're still using RestTemplate in new code, you're doing it wrong. RestTemplate is deprecated since Spring 5.0 and will be removed in Spring 6. WebClient is the way forward. It's reactive, non-blocking, and supports both synchronous and asynchronous calls. But more importantly, it gives you a fluent API for building URIs that is far less error-prone than string manipulation.
Here's the simplest possible WebClient GET request with a path variable:
``java WebClient webClient = WebClient.create("https://api.example.com"); Mono<User> user = webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(User.class); ``
Notice the {id} placeholder. WebClient automatically substitutes userId into the URI. This is safe, clean, and readable. But what about query parameters? The same pattern works:
``java webClient.get() .uri(uriBuilder -> uriBuilder.path("/users/{id}") .queryParam("include", "profile") .build(userId)) .retrieve() .bodyToMono(User.class); ``
This is the pattern you should use. Every. Single. Time. The uriBuilder lambda gives you a UriBuilder that handles encoding automatically. No more U or string concatenation.RLEncoder.encode()
Production Insight: I once saw a codebase where every WebClient call used string concatenation like uri + "?page=" + page + "&size=" + size. When the page parameter was supposed to be an integer but came as a string with spaces, the API started returning 400 errors. The fix took 5 minutes after we switched to uriBuilder. Don't be that team.
uriBuilder.path().queryParam().build() for all complex URI construction. Avoid string concatenation at all costs.Advanced Path Variable Patterns
Sometimes you need more than one path variable. Maybe you're calling a REST API like /api/v1/orders/{orderId}/items/{itemId}. WebClient handles this gracefully with the same placeholder syntax:
``java webClient.get() .uri("/orders/{orderId}/items/{itemId}", orderId, itemId) .retrieve() .bodyToMono(Item.class); ``
But what if you have a variable number of path segments? Or you need to conditionally include a path variable? The uriBuilder approach gives you full control:
``java webClient.get() .uri(uriBuilder -> { UriBuilder builder = uriBuilder.path("/orders/{orderId}/items"); if (itemId != null) { builder.path("/{itemId}"); } return builder.build(orderId, itemId); }) .retrieve() .bodyToMono(String.class); ``
Note that if itemId is null, you must still pass something for the placeholder. In this case, we build with orderId only, so the URI becomes /orders/123/items. If we had included {itemId} in the path, we'd need to pass both values.
Production Insight: I worked on a logistics platform that had a route like /shipments/{shipmentId}/tracking. The shipmentId was a UUID from the database. One day, the database returned a null for a shipment that was being deleted. The WebClient call threw a NullPointerException because shipmentId was null. We added a null check and a fallback to return a default response. Always validate path variables before building the URI.
uriBuilder for complex path logic. Always validate path variables before building.Query Parameters: The Right Way
Query parameters are the most common source of bugs in WebClient usage. The official documentation shows you the uriBuilder.queryParam() method, but it doesn't tell you about the edge cases. Let me break down the patterns you'll actually use in production.
1. Simple key-value pairs ``java uriBuilder.queryParam("page", 1) .queryParam("size", 20) ``
2. Multiple values for the same key Some APIs accept multiple values for the same parameter, like ?id=1&id=2&id=3. Use queryParam("id", List.of(1,2,3)) or call queryParam multiple times with the same key.
3. Optional parameters If a parameter might be null, you can conditionally add it: ``java if (sortBy != null) { uriBuilder.queryParam("sort", sortBy); } ``
4. Encoding uriBuilder automatically encodes values. But if you need to send a raw value (e.g., a pre-encoded string), you can use queryParam("key", value, false) to skip encoding. Use this sparingly.
5. Building the URI Always call .build() or .build(Object... pathVariables) at the end.
Here's a comprehensive example:
``java webClient.get() .uri(uriBuilder -> uriBuilder .path("/search") .queryParam("q", "spring webclient") .queryParam("page", 1) .queryParam("size", 20) .queryParamIfPresent("sort", Optional.ofNullable(sortBy)) .build()) .retrieve() .bodyToMono(SearchResult.class); ``
queryParamIfPresent is a neat helper that only adds the parameter if the Optional is non-empty. It's available since Spring 5.3.
queryParam() for required parameters and queryParamIfPresent() for optional ones. Never manually encode.What the Official Docs Won't Tell You
After years of debugging WebClient issues, here are the things I wish the Spring documentation covered:
1. The uri method with a lambda is not thread-safe. The UriBuilder instance passed to the lambda is reused across calls if you share the same WebClient instance. If you mutate it in a multi-threaded environment, you'll get race conditions. Always create a new WebClient per request or use WebClient.mutate() to create a fresh one. Alternatively, use UriComponentsBuilder outside the lambda and pass a pre-built URI.
2. without arguments vs with arguments If you use build() (no args), the URI must not have any unresolved path variables. If you have placeholders, you must use build()build(Object... vars). Forgetting to pass variables will cause an IllegalArgumentException at runtime.
3. Encoding of special characters WebClient's uriBuilder encodes query parameter values using application/x-www-form-urlencoded encoding. But some APIs expect different encoding (e.g., RFC 3986). If you need custom encoding, build the URI with UriComponentsBuilder and pass it to WebClient.
4. The vs retrieve() debate exchange() is simpler but doesn't give you access to the raw response. retrieve() is deprecated in Spring 5.3 and replaced by exchange() with retrieve()onStatus() for error handling. Use .retrieve()
5. Testing is harder than it should be Mocking WebClient calls in unit tests requires WebClientRequestHeaders and WebClientResponse mocks. Consider using WireMock or MockWebServer for integration tests. I'll cover testing in a later section.
retrieve() and exchange(). Test thoroughly.Error Handling and Status Codes
A WebClient call isn't complete without proper error handling. The method throws retrieve()WebClientResponseException for 4xx and 5xx responses by default. But you can customize this with onStatus():
``java webClient.get() .uri("/users/{id}", id) .retrieve() .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new ClientException(response.statusCode().value()))) .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new ServerException("Server error"))) .bodyToMono(User.class); ``
This is much cleaner than checking status codes manually. But there's a catch: if you use onStatus() for a specific status like 404, it overrides the default error handling for that status. Always include a fallback for unexpected statuses.
What about query parameters in error responses? Sometimes the server returns a 400 with a message that includes the invalid parameter. You can access the response body in the onStatus handler:
``java .onStatus(status -> status == HttpStatus.BAD_REQUEST, response -> response.bodyToMono(String.class).flatMap(body -> Mono.error(new BadRequestException(body)))) ``
This is invaluable for debugging. The body often contains a description of which parameter was invalid.
onStatus() for granular error handling. Include the response body in error messages for 4xx errors.Testing WebClient with Query Parameters
Testing WebClient calls is notoriously tricky because of the reactive nature. But you have options. The best approach is to use WireMock to mock the HTTP server. Here's how to test a request with path variables and query parameters:
```java @SpringBootTest @AutoConfigureWebTestClient class UserClientTest {
@Autowired private WebTestClient webTestClient;
@Test void testGetUser() { webTestClient.get() .uri("/users/{id}?include=profile", 1) .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$.name").isEqualTo("John"); } } ```
But if you're testing the actual WebClient logic (not the controller), you'll want to mock the external service. Here's a WireMock example:
```java @SpringBootTest @WireMockTest(httpPort = 8089) class UserServiceTest {
@Test void testGetUserWithQueryParam() { stubFor(get(urlPathEqualTo("/users/1")) .withQueryParam("include", equalTo("profile")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"name\":\"John\"}")));
WebClient webClient = WebClient.create("http://localhost:8089"); Mono<User> userMono = webClient.get() .uri(uriBuilder -> uriBuilder .path("/users/{id}") .queryParam("include", "profile") .build(1)) .retrieve() .bodyToMono(User.class);
User user = userMono.block(); assertEquals("John", user.getName()); } } ```
Notice how we use urlPathEqualTo and withQueryParam to match the exact URI. This ensures your WebClient is building the correct URI.
urlPathEqualTo and withQueryParam for precise matching.The Missing Query Parameter That Took Down a Payment Pipeline
uri + "?apiKey=" + apiKey + "&transactionId=" + txId. The apiKey contained a + character (base64 encoded), which was interpreted as a space by the server, causing authentication failure. The gateway returned a 200 with an error message inside JSON, which the client didn't check.uriBuilder.queryParam("apiKey", apiKey) which properly URL-encodes the value. Also added response validation to check for error codes in the response body.- Never concatenate query parameters manually. Always use
uriBuilderorUriComponentsBuilder. - Always URL-encode query parameter values. WebClient's
uriBuilderdoes this automatically. - Validate response bodies even for 2xx status codes – many APIs return errors with 200.
- Log the full request URI during debugging to spot encoding issues.
- Use a library like Feign or Retrofit if you need more declarative HTTP clients, but WebClient is fine for most cases.
logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACEuriBuilder.build(). Use uriBuilder.build(Map.of("id", Objects.requireNonNull(id))).WebClient.create().get().uri(...).retrieve().timeout(Duration.ofSeconds(5)).uriBuilder.toUriString() to see the exact string sent.logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACESystem.out.println(uriBuilder.build().toUriString())| File | Command / Code | Purpose |
|---|---|---|
| WebClientBasicExample.java | public class WebClientBasicExample { | WebClient Basics |
| AdvancedPathVariables.java | public class AdvancedPathVariables { | Advanced Path Variable Patterns |
| QueryParamsExample.java | public class QueryParamsExample { | Query Parameters |
| ThreadSafetyIssue.java | public class ThreadSafetyIssue { | What the Official Docs Won't Tell You |
| ErrorHandlingExample.java | public class ErrorHandlingExample { | Error Handling and Status Codes |
| WireMockTest.java | @WireMockTest(httpPort = 8089) | Testing WebClient with Query Parameters |
Key takeaways
uriBuilder with queryParam() and path() for all URI construction. Avoid string concatenation.queryParamIfPresent for optional parameters.onStatus() and include response bodies for 4xx errors.Interview Questions on This Topic
How do you add query parameters to a WebClient request? Provide a code example.
uri(Function<UriBuilder, URI>) overload and call queryParam() on the builder. Example: webClient.get().uri(b -> b.path("/search").queryParam("q", "spring").build()).retrieve().bodyToMono(String.class);Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't