What's Actually Happening
You hit a REST endpoint and get HTTP 415 Unsupported Media Type with this in the logs:
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded' not supported
Spring is telling you: the Content-Type the client sent doesn't match what the controller expects. Your endpoint uses @RequestBody, which requires JSON (or another structured format). The client is sending form-encoded data โ the same format browsers use when submitting plain HTML forms.
Three scenarios trigger this repeatedly: you're testing with a tool that defaults to form encoding, a frontend dev is calling the wrong content type, or someone migrated an old MVC controller to a REST controller without updating the request format.
Reproduce and Confirm the Root Cause
Pin down the mismatch by checking two things.
1. What the client is sending
This curl command looks innocent but sends form-encoded data by default:
curl -X POST http://localhost:8080/api/users \
-d "name=Alice&email=alice@example.com"
Spring's @RequestBody cannot deserialize that. No converter is registered for form data on @RequestBody endpoints โ that's the 415.
Using Postman or Insomnia? Check the Body tab. If it's set to form-data or x-www-form-urlencoded, that's your problem right there.
2. What the controller expects
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@RequestBody UserRequest request) {
// @RequestBody requires Content-Type: application/json
return ResponseEntity.ok(userService.create(request));
}
}
@RequestBody tells Spring to deserialize the HTTP body via a HttpMessageConverter. Spring Boot registers Jackson for JSON automatically. Form-encoded data has no matching converter โ hence the 415.
Fix 1 โ Change the Client to Send JSON (Most Common Fix)
If the controller is correct and you're building a proper REST API, fix the client side.
curl
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
Postman / Insomnia
- Open the Body tab
- Select raw
- Pick JSON from the dropdown
- Paste your JSON payload
JavaScript (fetch API)
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
});
Axios
// Axios sets Content-Type: application/json automatically when you pass a plain object
axios.post('/api/users', { name: 'Alice', email: 'alice@example.com' });
One trap with Axios: wrapping the data in qs.stringify() or URLSearchParams silently switches it back to form encoding. That's a common source of confusion when the Axios call looks correct at first glance.
Fix 2 โ Change the Controller to Accept Form Data
Some integrations send form-encoded POST requests by design โ payment gateways, OAuth callbacks, legacy webhooks. In those cases, swap @RequestBody for @RequestParam:
@PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<User> createUserFromForm(
@RequestParam String name,
@RequestParam String email) {
return ResponseEntity.ok(userService.create(name, email));
}
Got a DTO with a dozen fields? Use @ModelAttribute instead:
@PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<User> createUserFromForm(@ModelAttribute UserRequest request) {
return ResponseEntity.ok(userService.create(request));
}
The consumes attribute does double duty: it enforces the expected media type and makes your API self-documenting. Spring will return a clean 415 to any caller who sends the wrong format.
Fix 3 โ Support Both JSON and Form Encoding
Rare, but sometimes you're building a hybrid endpoint that needs to accept both formats โ for example, a login endpoint consumed by both a mobile app (JSON) and an old server-side form (form-encoded):
@PostMapping(
value = "/users",
consumes = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_FORM_URLENCODED_VALUE }
)
public ResponseEntity<User> createUser(
@RequestBody(required = false) UserRequest jsonRequest,
@ModelAttribute(binding = false) UserRequest formRequest,
@RequestHeader("Content-Type") String contentType) {
UserRequest request = contentType.contains("application/json") ? jsonRequest : formRequest;
return ResponseEntity.ok(userService.create(request));
}
This works, but it gets messy fast. Two separate endpoints โ one for REST clients, one for form-based clients โ is usually cleaner and easier to test.
Fix 4 โ Check Spring Security Filter (Unexpected 415s)
Sometimes the 415 appears even though your client is definitely sending JSON. Spring Security is a frequent culprit. Its CSRF filter can consume the request body before your controller sees it, especially in Spring Security 5.x/6.x. A filter further up the chain may also be rewriting headers.
For stateless REST APIs, disable CSRF:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // stateless REST โ no CSRF needed
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
}
Still not sure what's arriving at the controller? Add a logging filter to see the raw Content-Type before Spring processes it:
@Component
public class LoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
log.info("Content-Type: {}", request.getContentType());
filterChain.doFilter(request, response);
}
}
Verification
Run this and confirm you get a 2xx back:
curl -v -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
The -v flag shows the full response. Look for HTTP/1.1 200 OK or 201 Created. No more 415.
For integration tests, cover both the happy path and the rejection:
@Test
void createUser_withJsonBody_returns201() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"Alice\", \"email\": \"alice@example.com\"}"))
.andExpect(status().isCreated());
}
@Test
void createUser_withFormEncoding_returns415() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("name", "Alice")
.param("email", "alice@example.com"))
.andExpect(status().isUnsupportedMediaType());
}
Tips
Debugging Content-Type issues across services? URL encoding has a habit of sneaking in at unexpected places โ a middleware layer, a proxy, or a library doing the wrong thing. The URL Encoder/Decoder at ToolCraft lets you quickly decode what a third-party webhook is actually sending, without forwarding your data anywhere.
A few habits that prevent this class of bug entirely:
- Always set
consumeson@PostMapping. It turns a vague 400 into a clear 415 with a useful message. - Add an integration test per endpoint that verifies the correct Content-Type is enforced โ takes 10 minutes, saves hours of debugging.
- Wire up Springdoc OpenAPI (the successor to Springfox). It exposes expected media types in Swagger UI, so frontend developers know what to send before they even write the first request.
The Core Problem
HTTP 415 is a contract violation. The controller says what it accepts; the client must match it. @RequestBody means JSON. Form data needs @RequestParam or @ModelAttribute. The fix is almost always one line โ but finding which line requires knowing where the mismatch lives. Be explicit with consumes from day one, and the error surfaces immediately with a clear message instead of mutating into a confusing 400 or 500 somewhere downstream.

