Spring Boot
WebClient
Form Data
Application/x-www-form-urlencoded
HTTP POST Request

How to post request with spring boot web-client for Form data for content type application/x-www-form-urlencoded

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

When an API expects application/x-www-form-urlencoded, sending JSON is the wrong protocol even if the fields look similar. With Spring WebClient, the correct approach is to build a form body explicitly and set the content type so the remote server receives key=value pairs instead of a JSON document.

Why Form-Encoded Requests Are Different

A form-encoded body is not just "JSON with another header." The wire format is different:

  • JSON body: {"username":"alice","password":"secret"}
  • Form body: username=alice&password=secret

Servers that expect OAuth token requests, login forms, or older HTTP integrations usually validate both the body format and the Content-Type header. If either one is wrong, the request fails.

The Standard WebClient Pattern

Spring provides BodyInserters.fromFormData for this exact use case.

java
1import org.springframework.http.MediaType;
2import org.springframework.web.reactive.function.BodyInserters;
3import org.springframework.web.reactive.function.client.WebClient;
4
5public class FormPostDemo {
6    public static void main(String[] args) {
7        WebClient client = WebClient.builder()
8            .baseUrl("https://api.example.com")
9            .build();
10
11        String response = client.post()
12            .uri("/login")
13            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
14            .body(BodyInserters
15                .fromFormData("username", "alice")
16                .with("password", "secret"))
17            .retrieve()
18            .bodyToMono(String.class)
19            .block();
20
21        System.out.println(response);
22    }
23}

The important parts are the APPLICATION_FORM_URLENCODED content type and the form body inserter.

Building a MultiValueMap

If the request data comes from variables or collections, a MultiValueMap can be clearer.

java
1import org.springframework.http.MediaType;
2import org.springframework.util.LinkedMultiValueMap;
3import org.springframework.util.MultiValueMap;
4import org.springframework.web.reactive.function.BodyInserters;
5import org.springframework.web.reactive.function.client.WebClient;
6
7MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
8form.add("grant_type", "password");
9form.add("username", "alice");
10form.add("password", "secret");
11
12String response = WebClient.create("https://api.example.com")
13    .post()
14    .uri("/oauth/token")
15    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
16    .body(BodyInserters.fromFormData(form))
17    .retrieve()
18    .bodyToMono(String.class)
19    .block();

This is useful when the set of fields is assembled dynamically.

Mapping the Response to a DTO

Most real integrations do not want a raw string back. If the server responds with JSON, deserialize it directly.

java
1public record TokenResponse(String access_token, String token_type, long expires_in) {}
2
3TokenResponse token = WebClient.create("https://api.example.com")
4    .post()
5    .uri("/oauth/token")
6    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
7    .body(BodyInserters
8        .fromFormData("grant_type", "client_credentials")
9        .with("client_id", "my-client")
10        .with("client_secret", "top-secret"))
11    .retrieve()
12    .bodyToMono(TokenResponse.class)
13    .block();
14
15System.out.println(token.access_token());

That keeps the calling code cleaner and makes downstream validation easier.

Reactive and Blocking Styles

If your application is reactive end-to-end, do not call block(). Return the Mono instead.

java
1import reactor.core.publisher.Mono;
2
3Mono<TokenResponse> tokenMono = WebClient.create("https://api.example.com")
4    .post()
5    .uri("/oauth/token")
6    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
7    .body(BodyInserters
8        .fromFormData("grant_type", "client_credentials")
9        .with("client_id", "my-client")
10        .with("client_secret", "top-secret"))
11    .retrieve()
12    .bodyToMono(TokenResponse.class);

Blocking is acceptable in non-reactive command-line tools or imperative service layers, but it defeats the reactive model inside a WebFlux request chain.

Error Handling Matters

When the remote server rejects the form, the response body often explains exactly why. Capture that body rather than throwing a generic exception.

java
1TokenResponse token = WebClient.create("https://api.example.com")
2    .post()
3    .uri("/oauth/token")
4    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
5    .body(BodyInserters.fromFormData("grant_type", "client_credentials"))
6    .retrieve()
7    .onStatus(
8        status -> status.is4xxClientError() || status.is5xxServerError(),
9        response -> response.bodyToMono(String.class)
10            .map(body -> new IllegalStateException("Remote error: " + body))
11    )
12    .bodyToMono(TokenResponse.class)
13    .block();

That is especially useful when debugging OAuth servers and legacy HTTP integrations.

Common Pitfalls

The most common mistake is sending a Java object body and assuming WebClient will convert it into form data automatically. It will not; object bodies are typically serialized as JSON.

Another issue is forgetting to set MediaType.APPLICATION_FORM_URLENCODED, which causes the server to interpret the request incorrectly even if the fields were assembled as form data.

Developers also call block() inside a reactive request path without realizing they are reintroducing synchronous waiting into non-blocking code.

Finally, do not ignore error payloads. Many "mysterious" form-post failures are actually clear protocol mismatches once you inspect the remote response body.

Summary

  • Use BodyInserters.fromFormData for form-encoded POST requests.
  • Set Content-Type to application/x-www-form-urlencoded.
  • Use DTOs for structured JSON responses instead of parsing strings manually.
  • Return Mono in reactive code paths and avoid unnecessary block().
  • Read remote error bodies when debugging failed integrations.

Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design