Reputation: 91
Am writing a REST endpoint which needs to support both application/x-www-form-urlencoded and application/json as request body simultaneously. I have made below configuration,
@RequestMapping(method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE }, consumes = {
MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.APPLICATION_JSON_VALUE }, path = Constants.ACCESS_TOKEN_V1_ENDPOINT)
public OAuth2Authorization createAccessTokenPost(
@RequestBody(required = false) MultiValueMap<String, String> paramMap) { ..
While it supports application/x-www-form-urlencoded or application/json individually (when I comment out one content type from consumes = {}), but it does not support both simultaneously. Any ideas ?
Upvotes: 6
Views: 7336
Reputation: 71
So RestControllers by default can handle application/json
fairly easily and can create a request pojo from a @RequestBody
annotated parameter, while application/x-www-form-urlencoded
takes a little more work. A solution could be creating an extra RestController method that has the same mapping endpoint to handle the different kinds of requests that come in (application/json, application/x-www-form-urlencoded
, etc). This is because application/x-www-form-urlencoded
endpoints need to use the @RequestParam
instead of the @RequestBody
annotation (for application/json
).
For instance if I wanted to host a POST endpoint for /emp
that takes either application/json
or application/x-www-form-urlencoded
as Content-Types and uses a service to do something, I could create Overload methods like so
@Autowired
private EmpService empService;
@PostMapping(path = "/emp", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity createEmp(final @RequestHeader(value = "Authorization", required = false) String authorizationHeader,
final @RequestParam Map<String, String> map) {
//After receiving a FORM URLENCODED request, change it to your desired request pojo with ObjectMapper
final ObjectMapper mapper = new ObjectMapper();
final TokenRequest tokenRequest = mapper.convertValue(map, CreateEmpRequest.class);
return empService.create(authorizationHeader, createEmpRequest);
}
@PostMapping(path = "/emp", consumes = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity createEmp(final @RequestHeader(value = "Authorization", required = false) String authorizationHeader,
final @RequestBody CreateEmpRequest createEmpRequest) {
//Receieved a JSON request, the @RequestBody Annotation can handle turning the body of the request into a request pojo without extra lines of code
return empService.create(authorizationHeader, createEmpRequest);
}
Upvotes: 5
Reputation: 652
Just to make it, the above answer doesn't work as even if you do not annotate MultiValueMap
with @RequestBody
it would always check for contentType==MediaType.APPLICATION_FORM_URLENCODED_VALUE
which again in rest of the cases resolves to 415 Unsupported Media Type
.
Upvotes: 1
Reputation: 3
It's not possible to handle application/json
and application/x-www-form-urlencoded
requests simultaneously with a single Spring controller method.
Spring get application/x-www-form-urlencoded
data by ServletRequest.getParameter(java.lang.String), the document said:
For HTTP servlets, parameters are contained in the query string or posted form data.
If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.
So, if your method parameter is annotated with @RequestBody
, Spring will read request body and parse it to the method parameter object. But application/x-www-form-urlencoded
leads Spring to populate the parameter object by invoking ServletRequest.getParameter(java.lang.String).
Upvotes: 0
Reputation: 91
As per my findings, spring does not support content types "application/x-www-form-urlencoded
", "application/json
" and "application/xml
" together.
Reason I figured: Spring processes JSON and XML types by parsing and injecting them into the java pojo marked with @RequestBody
spring annotation. However, x-www-form-urlencoded
must be injected into a MultiValueMap<>
object marked with @RequestBody
. Two different java types marked with @RequestBody
will not be supported simultaneously, as spring may not know where to inject the payload.
A working solution:
"application/x-www-form-urlencoded
" can be supported as it is in the API. That is, it can be injected into spring's MultiValueMap<>
using an @RequestBody annotation.
To support JSON and XML on the same method, we can leverage servlet specification and spring's class built on top of them to extract the payload as stream.
Sample code:
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.MultiValueMap;
// usual REST service class
@Autowired
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;
@Autowired
private Jaxb2RootElementHttpMessageConverter jaxb2RootElementHttpMessageConverter;
public ResponseEntity<Object> authorizationRequestPost(HttpServletResponse response, HttpServletRequest request,@RequestBody(required = false) MultiValueMap<String, String> parameters) {
// this MultiValueMap<String,String> will contain key value pairs of "application/x-www-form-urlencoded" parameters.
// payload object to be populated
Authorization authorization = null;
HttpInputMessage inputMessage = new ServletServerHttpRequest(request) {
@Override
public InputStream getBody() throws IOException {
return request.getInputStream();
}
};
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
authorization = (Authorization) mappingJackson2HttpMessageConverter.read(Authorization.class, inputMessage);
}
else if (request.getContentType().equals(MediaType.APPLICATION_XML_VALUE)) {
authorization = (Authorization)jaxb2RootElementHttpMessageConverter.read(Authorization.class, inputMessage);
}
else{
// extract values from MultiValueMap<String,String> and populate Authorization
}
// remaining method instructions
}
Point to note that any custom data type/markup/format can be supported using this approach. Spring's org.springframework.http.converter.HttpMessageConverter<>
can be extended to write the parsing logic.
Another possible approach could be an AOP style solution which would execute the same logic: parse payload by extracting it from HttpServlet
input stream and inject into the payload object.
A third approach will be to write a filter for executing the logic.
Upvotes: 2