Consuming MULTIPART_FORM_DATA and application/x-www-form-urlencoded Media Types in a method of jersey servlet - jersey

I have a method in jersey servlet which consumes both MULTIPART_FORM_DATA and application/x-www-form-urlencoded Media Types, In my request I am sending some parameters along with a file in the file input stream.
Here is my method
#POST
#Path("/upload")
#Consumes({MediaType.MULTIPART_FORM_DATA,MediaType.APPLICATION_FORM_URLENCODED})
#Produces(MediaType.TEXT_PLAIN)
public String uploadFile(MultivaluedMap<String,String> requestParamsPost,#FormDataParam("file") InputStream fis,
#FormDataParam("file") FormDataContentDisposition fdcd){
//some code goes here
}
But my problem is when I start my server after making the mapping of the servlet in web.xml, I get some severe exceptions
SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.package.ImportService.uploadFile(java.lang.String,java.lang.String,java.lang.String) at parameter at index 0
SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.package.ImportService.uploadFile(java.lang.String,java.lang.String,java.lang.String) at parameter at index 1
SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.package.ImportService.uploadFile(java.lang.String,java.lang.String,java.lang.String) at parameter at index 2
Is it somehow possible to consume two Media Types in one method at single endpoint?
Sending a file Parameter is necessary in every request?

The reason for the error is the MultivaluedMap parameter. Jersey doesn't know what to do with it. You can only have one entity type per method. In your method you are trying to accept two different body types in the request. You can't do that. I don't even know how you plan on sending that from the client.
The application/x-www-form-urlencoded data needs to be part of the multipart body. So you can do
#Consumes({MediaType.MULTIPART_FORM_DATA})
public String uploadFile(#FormDataParam("form-data") MultivaluedMap<String,String> form,
#FormDataParam("file") InputStream fis,
#FormDataParam("file") FormDataContentDisposition fdcd){
That would work. The only thing is, you need to make sure the client set the Content-Type of the form-data part to application/x-www-form-urlencoded. If they don't, then the default Content-Type for that part will be text/plain and Jersey will not be able to parse it to a MultivaluedMap.
What you can do instead is just use FormDataBodyPart as a method parameter, then explicitly set the media type. Then you can extract it to a MultivaluedMap. This way the client doesn't need to be expected to set the Content-Type for that part. Some clients don't even allow for setting individual part types.
Here's an example using Jersey Test Framework
import java.util.logging.Logger;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
public class MultipartTest extends JerseyTest {
#Path("test")
public static class MultiPartResource {
#POST
#Consumes(MediaType.MULTIPART_FORM_DATA)
public Response post(#FormDataParam("form-data") FormDataBodyPart bodyPart,
#FormDataParam("data") String data) {
bodyPart.setMediaType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
MultivaluedMap<String, String> formData = bodyPart.getEntityAs(MultivaluedMap.class);
StringBuilder sb = new StringBuilder();
sb.append(data).append(";").append(formData.getFirst("key"));
return Response.ok(sb.toString()).build();
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(MultiPartResource.class)
.register(MultiPartFeature.class)
.register(new LoggingFilter(Logger.getAnonymousLogger(), true));
}
#Override
public void configureClient(ClientConfig config) {
config.register(MultiPartFeature.class);
}
#Test
public void doit() {
FormDataMultiPart multiPart = new FormDataMultiPart();
multiPart.field("data", "hello");
multiPart.field("form-data", "key=world");
final Response response = target("test")
.request().post(Entity.entity(multiPart, MediaType.MULTIPART_FORM_DATA));
assertEquals("hello;world", response.readEntity(String.class));
}
}
If you look at the logging, you will see the request as
--Boundary_1_323823279_1458137333706
Content-Type: text/plain
Content-Disposition: form-data; name="data"
hello
--Boundary_1_323823279_1458137333706
Content-Type: text/plain
Content-Disposition: form-data; name="form-data"
key=world
--Boundary_1_323823279_1458137333706--
You can see the Content-Type for the form-data body part is text/plain, which is the default, but in the server side, we explicitly set it before Jersey parses it
public Response post(#FormDataParam("form-data") FormDataBodyPart bodyPart,
#FormDataParam("data") String data) {
bodyPart.setMediaType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
MultivaluedMap<String, String> formData = bodyPart.getEntityAs(MultivaluedMap.class);

Related

Can no longer obtain form data from HttpServletRequest SpringBoot 2.2, Jersey 2.29

We have a SpringBoot application and are using Jersey to audit incoming HTTP requests.
We implemented a Jersey ContainerRequestFilter to retrieve the incoming HttpServletRequest
and use the HttpServletRequest's getParameterMap() method to extract both query and form data and place it in our audit.
This aligns with the javadoc for the getParameterMap():
"Request parameters are extra information sent with the request. For
HTTP servlets, parameters are contained in the query string or posted
form data."
And here is the documentation pertaining to the filter:
https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/user-guide.html#filters-and-interceptors
Upon updating SpringBoot, we found that the getParameterMap() no longer returned form data, but still returned query data.
We found that SpringBoot 2.1 is the last version to support our code. In SpringBoot 2.2 the version of Jersey was updated 2.29, but upon reviewing the release notes we don't see anything related to this.
What changed? What would we need to change to support SpringBoot 2.2 / Jersey 2.29?
Here is a simplified version of our code:
JerseyRequestFilter - our filter
import javax.annotation.Priority;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
...
#Provider
#Priority(Priorities.AUTHORIZATION)
public class JerseyRequestFilter implements ContainerRequestFilter {
#Context
private ResourceInfo resourceInfo;
#Context
private HttpServletRequest httpRequest;
...
public void filter(ContainerRequestContext context) throws IOException {
...
requestData = new RequestInterceptorModel(context, httpRequest, resourceInfo);
...
}
...
}
RequestInterceptorModel - the map is not populating with form data, only query data
import lombok.Data;
import org.glassfish.jersey.server.ContainerRequest;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ResourceInfo;
...
#Data
public class RequestInterceptorModel {
private Map<String, String[]> parameterMap;
...
public RequestInterceptorModel(ContainerRequestContext context, HttpServletRequest httpRequest, ResourceInfo resourceInfo) throws AuthorizationException, IOException {
...
setParameterMap(httpRequest.getParameterMap());
...
}
...
}
JerseyConfig - our config
import com.xyz.service.APIService;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.jaxrs.listing.SwaggerSerializers;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.wadl.internal.WadlResource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
...
#Component
public class JerseyConfig extends ResourceConfig {
...
public JerseyConfig() {
this.register(APIService.class);
...
// Access through /<Jersey's servlet path>/application.wadl
this.register(WadlResource.class);
this.register(AuthFilter.class);
this.register(JerseyRequestFilter.class);
this.register(JerseyResponseFilter.class);
this.register(ExceptionHandler.class);
this.register(ClientAbortExceptionWriterInterceptor.class);
}
#PostConstruct
public void init()
this.configureSwagger();
}
private void configureSwagger() {
...
}
}
Full Example
Here are the steps to recreate with our sample project:
download the source from github here:
git clone https://github.com/fei0x/so-jerseyBodyIssue
navigate to the project directory with the pom.xml file
run the project with:
mvn -Prun
in a new terminal run the following curl command to test the web service
curl -X POST \
http://localhost:8012/api/jerseyBody/ping \
-H 'content-type: application/x-www-form-urlencoded' \
-d param=Test%20String
in the log you will see the form parameters
stop the running project, ctrl-C
update the pom's parent version to the newer version of SpringBoot
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>
to
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.9.RELEASE</version>
run the project again:
mvn -Prun
invoke the curl call again:
curl -X POST \
http://localhost:8012/api/jerseyBody/ping \
-H 'content-type: application/x-www-form-urlencoded' \
-d param=Test%20String
This time the log will be missing the form parameters
Alright, after a ton of debugging code and digging through github repos I found the following:
There is a filter, that reads the body inputstream of the request if it is a POST request, making it unusable for further usage. This is the HiddenHttpMethodFilter. This filter, however, puts the content of the body, if it is application/x-www-form-urlencoded into the requests parameterMap.
See this github issue: https://github.com/spring-projects/spring-framework/issues/21439
This filter was active by default in spring-boot 2.1.X.
Since this behavior is unwanted in most cases, a property was created to enable/disable it and with spring-boot 2.2.X it was deactivated by default.
Since your code relies on this filter, you can enable it via the following property:
spring.mvc.hiddenmethod.filter.enabled=true
I tested it locally and it worked for me.
Edit:
Here is what makes this solution work:
The HiddenHttpMethodFilter calls
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
String paramValue = request.getParameter(this.methodParam);
...
request.getParameter checks if the parameters have already been parsed and does so, if not the case.
At this time, the request body input stream has not been called yet, so the request figures to parse the body aswell:
org.apache.catalina.connector.Request#parseParameters
protected void parseParameters() {
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
...
// this is the bit that parses the actual query parameters
parameters.handleQueryParameters();
// here usingInputStream is false, and so the body is parsed aswell
if (usingInputStream || usingReader) {
success = true;
return;
}
... // the actual body parsing is done here
The thing is, that usingInputStream in this scenario is false and so the method does not return after parsing query params.
usingInputStream is only set to true when the input stream of the request body is retrieved for the first time. That is only done after we fall off the end of the filterChain and servicing the request. The inputStream is called when jersey initializes the ContainerRequest in org.glassfish.jersey.servlet.WebComponent#initContainerRequest
private void initContainerRequest(
final ContainerRequest requestContext,
final HttpServletRequest servletRequest,
final HttpServletResponse servletResponse,
final ResponseWriter responseWriter) throws IOException {
requestContext.setEntityStream(servletRequest.getInputStream());
...
Request#getInputStream
public ServletInputStream getInputStream() throws IOException {
...
usingInputStream = true;
...
Since the HiddenHttpMethodFilter is the only filter to access the parameters, without this filter the parameters are never parsed until we call request.getParameterMap() in RequestInterceptorModel. But at that time, the inputStream of the request body has already been accessed and so it
I will post this answer, even though #Amir Schnell already posted a working solution. The reason is that I am not quite sure why that solution works. Definitely, I would rather have a solution that just requires adding a property to a property file, as opposed to having to alter code as my solution does. But I am not sure if I am comfortable with a solution that works opposite of how my logic sees it's supposed to work. Here's what I mean. In your current code (SBv 2.1.15), if you make a request, look at the log and you will see a Jersey log
2020-12-15 11:43:04.858 WARN 5045 --- [nio-8012-exec-1] o.g.j.s.WebComponent : A servlet request to the URI http://localhost:8012/api/jerseyBody/ping contains form parameters in the request body but the request body has been consumed by the servlet or a servlet filter accessing the request parameters. Only resource methods using #FormParam will work as expected. Resource methods consuming the request body by other means will not work as expected.
This has been a known problem with Jersey and I have seen a few people on here asking why they can't get the parameters from the HttpServletRequest (this message is almost always in their log). In your app though, even though this is logged, you are able to get the parameters. It is only after upgrading your SB version, and then not seeing the log, that the parameters are unavailable. So you see why I am confused.
Here is another solution that doesn't require messing with filters. What you can do is use the same method that Jersey uses to get the #FormParams. Just add the following method to your RequestInterceptorModel class
private static Map<String, String[]> getFormParameterMap(ContainerRequestContext context) {
Map<String, String[]> paramMap = new HashMap<>();
ContainerRequest request = (ContainerRequest) context;
if (MediaTypes.typeEqual(MediaType.APPLICATION_FORM_URLENCODED_TYPE, request.getMediaType())) {
request.bufferEntity();
Form form = request.readEntity(Form.class);
MultivaluedMap<String, String> multiMap = form.asMap();
multiMap.forEach((key, list) -> paramMap.put(key, list.toArray(new String[0])));
}
return paramMap;
}
You don't need the HttpServletRequest at all for this. Now you can set your parameter map by calling this method instead
setParameterMap(getFormParameterMap(context));
Hopefully someone can explain this baffling case though.

405 MethodNotAllowed is returned instead of what is specified in ResponseStatusException()

I have a very simple endpoint
#PostMapping("/exception")
public String exception() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
in 2 different machines. On the first machine this code is in a very simple spring boot app and it works as it is supposed to be working - when invoked, it returns 400 BAD_REQUEST. On the second machine, I have real spring boot project, with a lot of stuff. There, instead of having BAD_REQUEST returned, i get 405 MethodNotAllowed.
I don't even know what can be causing this behavior. Do you have any idea what is the case?
I am attaching a screenshot of the postman request that I use.
Postman screenshot
The whole controller:
package com.xxx.service.max.web.controller;
import com.xxx.service.max.model.context.UserContext;
import com.xxx.service.max.services.cas.CustomerAccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import static com.xxx.service.max.constant.Constants.MY_ACCOUNT_X_REST;
#RestController
#RequestMapping(MY_ACCOUNT_X_REST)
public class ChangeLocaleController {
private static final Logger LOG = LoggerFactory.getLogger(ChangeLocaleController.class);
private UserContext userContext;
private CustomerAccountService customerAccountService;
#PostMapping("/exception")
public String exception() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
#Autowired
public void setUserContext(UserContext userContext) {
this.userContext = userContext;
}
#Autowired
public void setCustomerAccountService(CustomerAccountService customerAccountService) {
this.customerAccountService = customerAccountService;
}
}
Make sure you are sending a POST request.
The 405 Method Not Allowed error occurs when the web server is configured in a way that does not allow you to perform a specific action for a particular URL. It's an HTTP response status code that indicates that the request method is known by the server but is not supported by the target resource.
Source
If you are simply entering the URL in your browser that is a GET request and you would get a 405.

Spring Boot Webflux - Set UTF-8 Encoding

I've been working with Spring Boot 2.0.0.RC1 and use spring-boot-starter-webflux in order to build a REST Controller that returns a flux of text data.
#GetMapping(value = "/")
public Flux<String> getData(){
return Flux.interval(Duration.ofSeconds(2))
.map(l -> "Some text with umlauts (e.g. ä, ö, ü)...");
}
Since the text data contains some umlauts (e.g. ä, ö, ü), I would like to change the Content-Type header of the response from text/event-stream to text/event-stream;charset=UTF-8. Therefore, I tried wrapping to flux into a ResponseEntity. Like this:
#GetMapping(value = "/")
public ResponseEntity<Flux<String>> getData(){
return ResponseEntity
.ok()
.contentType(MediaType.parseMediaType("text/event-stream;charset=UTF-8"))
.body(Flux.interval(Duration.ofSeconds(2))
.map(l -> "Some text with umlauts (e.g. ä, ö, ü)..."));
}
Now, making a curl request to the endpoint shows that the Content-Type remains the same:
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Content-Type: text/event-stream
<
data:Some text with umlauts (e.g. ├ñ, ├Â, ├╝)...
I suspected the MediaType.parseMediaType() method to be the issue, but the media type is parsed correctly (as this screenshot shows):
However, the parameter charset seems to be ignored. How can I change the encoding to UTF-8 so that the browser interprets the umlaut characters correctly?
EDIT: Setting within the GetMapping annotation the produces field does not work either.
#GetMapping(value = "/", produces = "text/event-stream;charset=UTF-8")
public ResponseEntity<Flux<String>> getData(){
return ResponseEntity
.accepted()
.contentType(MediaType.parseMediaType("text/event-stream;charset=UTF-8"))
.body(Flux.interval(Duration.ofSeconds(2))
.map(l -> "Some text with umlauts (e.g. ä, ö, ü)..."));
}
You can create a Filter and process response before this return to browser
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.core.Ordered;
// esse filtro foi criado pra converter para UTF-8 o response do Flux<ServerSentEvent<String>>
// this filter was created to convert all responses to UTF8, including Flux<ServerSentEvent<String>>
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class FluxPreProcessorFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}

Supporting application/json and application/x-www-form-urlencoded simultaneously from Spring's rest controller

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 ?
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);
}
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.
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).
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.

Catch Exception In Spring Web Service Interceptor and return Soap response

What is the best approach to catch an exception in a Spring Web Service, extract the details of it, and format it into a soap response? My error message details must go in the header of the Soap response.
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ims="http://www.imsglobal.org/services/lis/cmsv1p0/wsdl11/sync/imscms_v1p">
<soapenv:Header>
<imsx_syncResponseHeaderInfo xmlns="http://www.imsglobal.org/services/lis/cmsv1p0/wsdl11/sync/imscms_v1p0">
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>4</imsx_messageIdentifier>
<imsx_statusInfo>
<imsx_codeMajor>failure</imsx_codeMajor>
<imsx_severity>status</imsx_severity>
<imsx_codeMinor>
<imsx_codeMinorField>
<imsx_codeMinorFieldName>TargetEndSystem</imsx_codeMinorFieldName>
<imsx_codeMinorFieldValue>incompletedata</imsx_codeMinorFieldValue>
</imsx_codeMinorField>
</imsx_codeMinor>
</imsx_statusInfo>
</imsx_syncResponseHeaderInfo>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
I do know if it is the best approach but I added a SimpleSoapExceptionResolver object:
import java.util.Date;
import java.util.Locale;
import org.apache.log4j.Logger;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapBody;
import org.springframework.ws.soap.SoapFault;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.server.endpoint.SimpleSoapExceptionResolver;
public final class MySimpleSoapExceptionResolver
extends SimpleSoapExceptionResolver {
public MySimpleSoapExceptionResolver () {
super.setOrder(HIGHEST_PRECEDENCE);
}
#Override
protected void customizeFault( final MessageContext messageContext_,
final Object endpoint_,
final Exception exception_,
SoapFault soapFault_) {
WebServiceMessage _webServiceMessageResponse =
messageContext_.getResponse();
SoapMessage _soapMessage = (SoapMessage) _webServiceMessageResponse;
SoapBody _soapBody = _soapMessage.getSoapBody();
String _message = "your error message";
Logger _logger = Logger.getLogger(MySimpleSoapExceptionResolver.class);
_logger.error(_message, exception_);
soapFault_ =
_soapBody.addServerOrReceiverFault(_message, Locale.ENGLISH);
}
}
You can probably implement a interceptor of type org.springframework.ws.server.endpoint.interceptor.EndpointInterceptorAdapter. Register your interceptor in your webservice configuration.
Implement the method handleResponse(MessageContext messageContext, Object endpoint) like this -
handleResponse(MessageContext messageContext, Object endpoint) {
SoapMessage msg = (SoapMessage) messageContext.getResponse();
SoapHeader header = msg.getSoapHeader();
// do what you want to do with header.
}
I have not implemented this but done similar stuff with interceptors in CXF.

Resources