I have a global exception handler as below :-
#ControllerAdvice
#RestController
public class GlobalExceptionHandler {
#ExceptionHandler(value= {HttpMessageNotReadableException.class})
public final ResponseEntity<ErrorDetails> validationException(HttpMessageNotReadableException ex, WebRequest request) {
System.out.println("This was called.");
if(ex.getCause() instanceof CsvRequiredFieldEmptyException){
CsvRequiredFieldEmptyException csvExp = (CsvRequiredFieldEmptyException) ex.getCause();
String exceptionDtls = csvExp.getMessage().concat(" ").concat(" at line number "+csvExp.getLineNumber()+ " in the csv filw.");
ErrorDetails errorDetails = new ErrorDetails(LocalDate.now(),exceptionDtls, request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
}
I am invoking the rest API by using TestRestTemplate for integration testing.
ResponseEntity<?> response = restTemplate.exchange(ITestUtils.createURLWithPort(postUrlCsv,
host,port ), HttpMethod.POST,listingDocEnt, String.class);
#Test
public void uploadListingCsvTest_Returns400BadReq_WhenCodeMissing() throws HttpMessageNotReadableException {
// Step 1 : Create the Http entity object which contains the request body and headers.
HttpEntity<ListingList> listingDocEnt = new HttpEntity<ListingList>(createTestDataForNewVehicleListingCodeMissing(),
getHttpHeaderCsv());
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
List<HttpMessageConverter<?>> csvMessgeonverter = new ArrayList<>();
csvMessgeonverter.add(new CsvHttpMessageConverter<>());
csvMessgeonverter.add(converter);
TestRestTemplate restTemplate = new TestRestTemplate();
restTemplate.getRestTemplate().setMessageConverters(csvMessgeonverter);
ResponseEntity<?> response = restTemplate.exchange(ITestUtils.createURLWithPort(postUrlCsv,
host,port ), HttpMethod.POST,listingDocEnt, String.class);
// Check if the response is not null and the http status code is - 201 Created.
Assert.assertNotNull(response);
Assert.assertEquals(HttpStatus.BAD_REQUEST,response.getStatusCode());
}
My rest API has custom HttpMessageConverter which is as below which converts the input request csv to java object in the rest controller. This custom message converter has a method readInternal which throws an exception HttpMessageNotReadableException , but still the exception handler method 'validationException' is not getting invoked. The Junit simply breaks and fails.
public class CsvHttpMessageConverter<T, L extends ListParam<T>>
extends AbstractHttpMessageConverter<L> {
public CsvHttpMessageConverter () {
super(new MediaType("text", "csv"));
}
#Override
protected boolean supports (Class<?> clazz) {
return ListParam.class.isAssignableFrom(clazz);
}
#Override
protected L readInternal (Class<? extends L> clazz,HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
HeaderColumnNameMappingStrategy<T> strategy = new HeaderColumnNameMappingStrategy<>();
Class<T> t = toBeanType(clazz.getGenericSuperclass());
strategy.setType(t);
CSVReader csv = new CSVReader(new InputStreamReader(inputMessage.getBody()));
CsvToBean<T> csvToBean = new CsvToBean<>();
List<T> beanList = null;
try {
beanList = csvToBean.parse(strategy, csv);
} catch(Exception exception){
throw new HttpMessageNotReadableException("Exception while parsing the CSV file.",exception.getCause());
}
try {
L l = clazz.newInstance();
l.setList(beanList);
return l;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
#SuppressWarnings("unchecked")
#Override
protected void writeInternal (L l, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
HeaderColumnNameMappingStrategy<T> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(toBeanType(l.getClass().getGenericSuperclass()));
OutputStreamWriter outputStream = new OutputStreamWriter(outputMessage.getBody());
StatefulBeanToCsv<T> beanToCsv =
new StatefulBeanToCsvBuilder(outputStream)
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.withMappingStrategy(strategy)
.build();
try {
beanToCsv.write(l.getList());
} catch (CsvDataTypeMismatchException e) {
throw new HttpMessageNotWritableException("Exception while parsing the CSV file.",e);
} catch (CsvRequiredFieldEmptyException e) {
throw new HttpMessageNotWritableException("Exception while parsing the CSV file.",e);
}
outputStream.close();
}
#SuppressWarnings("unchecked")
private Class<T> toBeanType (Type type) {
return (Class<T>) ((ParameterizedType) type).getActualTypeArguments()[0];
}
Is there a way that when invoking a spring rest API using TestRestTemplate we can invoke the exception handler method when there is an exception?
I think the problem here is that the HttpMessageNotReadableException is not thrown by the Controller, but instead by the spring infrastructure BEFORE the controller is invoked.
But a #ControllerAdvice does only handle exceptions that are thrown by the Controller.
In spring's DispatcherServlet there is also an ErrorHandler that is invoked in such cases. Maybe this is a solution for you?
Here are some infos about this:
https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#going-deeper
Related
I'm creating a component class that overrides a reactive method that calls another microservice "uaa" that validates a token, but when I verify that the token is invalid I throw an exception, but that exception does not catch in my exception controller handler
here is my component class
#Slf4j
#Component
#RequiredArgsConstructor
public class AuthFilter implements GlobalFilter {
private final JwtTokenProviderService jwtTokenProviderService;
private final TokenStatusDaoService tokenStatusDaoService;
private final WebClient webClient;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("something in the way");
List<String> headers = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION);
if(CollectionUtils.isEmpty(headers)) {
log.trace("Request came without token");
return chain.filter(exchange);
} else {
String authToken = headers.get(0);
log.trace("Request holds a token");
log.debug("Check if token has expired ...");
if(jwtTokenProviderService.isTokenExpired(authToken)) {
log.debug("Token has expired will throw an error");
throw new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_HAS_EXPIRED, "Token has expired");
}else {
log.debug("Check if token is valid and already saved");
String userId = jwtTokenProviderService.getClaimsFromToken(authToken).get(SecurityUtils.IDENTIFIER_KEY).toString();
if(!tokenStatusDaoService.exists(TokenStatusSpecification.withToken(authToken).and(TokenStatusSpecification.withUserId(Long.parseLong(userId))))) {
return webClient.get()
.uri("http://uaa", uriBuilder -> uriBuilder
.path("/validate-token")
.queryParam("token", authToken).build()).retrieve()
.bodyToMono(TokenValidationGetResource.class)
.map(tokenValidationGetResource -> {
if (!tokenValidationGetResource.isValid()) {
log.debug("token is not valid");
throw new AuthorizationForbiddenException(AuthorizationForbiddenExceptionTitleEnum.TOKEN_NOT_VALID, "Token is not valid");
} else {
log.debug("token is valid");
TokenStatusEntity tokenStatusEntity;
try {
tokenStatusEntity = tokenStatusDaoService.findOne(TokenStatusSpecification.withUserId(Long.parseLong(userId)));
} catch (Exception e) {
log.debug("No token defined for user: {}. Will save a new one ...", userId);
tokenStatusEntity = new TokenStatusEntity();
}
tokenStatusEntity.setToken(authToken);
tokenStatusEntity.setUserId(Long.parseLong(userId));
tokenStatusEntity.setStatus(TokenStatusEnum.VALID);
tokenStatusDaoService.save(tokenStatusEntity);
log.debug("Token status entity: {}", tokenStatusEntity);
return exchange;
}
}).flatMap(chain::filter);
} else {
log.debug("Token exists in DB");
return chain.filter(exchange);
}
}
}
}
}
and here is my exception controller handler:
#ControllerAdvice
public class ExceptionControllerImpl implements ExceptionController {
#Override
#ExceptionHandler({
AuthorizationForbiddenException.class
})
public ResponseEntity<ErrorDetailResource> handleGenericExceptions(
AbstractBaseException e, HttpServletRequest request) {
ErrorDetailResource errorDetailResource = new ErrorDetailResource();
errorDetailResource.setTimestamp(Instant.now().toEpochMilli());
errorDetailResource.setTitle(e.getTitle().toString());
errorDetailResource.setCode(e.getTitle().getCode());
errorDetailResource.setDeveloperMessage(e.getClass().getName());
errorDetailResource.setStatus(e.getStatus().value());
errorDetailResource.setDetail(e.getMessage());
return new ResponseEntity<>(errorDetailResource, e.getStatus());
}
}
Hello Those exceptions are thrown on a mono method in a reactive manner, so they can not be caught by controller advice, instead of doing that create a class which will extends the abstract class AbstractErrorWebExceptionHandler
#Component
#Order(-2)
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
public GlobalErrorWebExceptionHandler(GlobalErrorAttributes globalErrorAttributes,
ApplicationContext applicationContext,
ServerCodecConfigurer serverCodecConfigurer) {
super(globalErrorAttributes, new WebProperties.Resources(), applicationContext);
super.setMessageWriters(serverCodecConfigurer.getWriters());
super.setMessageReaders(serverCodecConfigurer.getReaders());
}
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
final Map<String, Object> errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults());
Throwable error = null;
// here is your abstract base exception
AbstractBaseException baseException = null;
try {
baseException = (AbstractBaseException) getError(request);
} catch (Exception e) {
error = getError(request);
}
HttpStatus statusCode = baseException != null ? baseException.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
return ServerResponse.status(statusCode)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
}
And of course do not forget to add DefaultErrorAttributes
#Component
public class GlobalErrorAttributes extends DefaultErrorAttributes {
#Override
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Throwable error = null;
// here is your abstract base exception
// cast the error to your exception class
AbstractBaseException baseException = null;
try {
baseException = (AbstractBaseException) getError(request);
} catch (Exception e) {
error = getError(request);
}
Map<String, Object> errorResources = new HashMap<>();
// Define the attribute that you want to return in response body
errorResources.put("attribute1", Instant.now().toEpochMilli());
errorResources.put("attribute2", baseException != null ? baseException.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR);
return errorResources;
}
}
I have a scenario to create a rest method which return json response if there is any validation fail. and if everything is correct then download a pdf method.
Is it possible to be done via same method?
PFB the method signature what i need to achieve.
#RequestMapping(value = "/getPdf", method = RequestMethod.POST, consumes = MediaType.ALL_VALUE, produces = {"application/pdf","application/json"})
public #ResponseBody LinkedHashMap<String, Object> getPdf(#Valid #RequestBody DownloadPdfDTO downloadPdfDTO,BindingResult result,HttpServletRequest request,HttpServletResponse response) throws IOException {
}
This Method works fine when everything correct and PDF is getting download.
But when there is any validation fail then no response is getting render and i am getting Status : 406 Not Acceptable.
Thanks
Yes u can dot it using global exception handler.
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class GlobalExceptionHandler {
ObjectMapper objectMapper = new ObjectMapper();
#ExceptionHandler(AccessDeniedException.class)
public void handleAccessDenied(HttpServletResponse response, AccessDeniedException ex) {
response.setStatus(HttpStatus.UNAUTHORIZED);
try (PrintWriter writer = response.getWriter()) {
objectMapper.writeValue(writer, YOUR_OBJECT_TO_RETURN);
} catch (IOException ex) {
}
}
#ExceptionHandler({MethodArgumentNotValidException.class, ConstraintViolationException.class})
public void handleException(HttpServletResponse response, Exception ex) {
response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY);
try (PrintWriter writer = response.getWriter()) {
objectMapper.writeValue(writer, YOUR_OBJECT_TO_RETURN);
} catch (IOException ex) {
}
}
#ExceptionHandler(NullPointerException.class)
public void handleNullPointerException(HttpServletResponse response, Exception ex) {
//return your required response
}
}
You can give a return type as ResponseEntity. With ResponseEntity you can bind different response as per your business logic.
An example will be like this
#ApiOperation(value = "Get PDF API", consumes = "application/json", httpMethod = "GET")
#ApiResponses(value = {#ApiResponse(code = 404, message = "Bad request")})
#RequestMapping(value = "/pdf/{filename}", headers = "Accept=application/json", method = RequestMethod.GET)
public
#ResponseBody
ResponseEntity getPDF(#ApiParam(value = "filename", required = true) #PathVariable String filename) {
try {
Boolean validationPass = checkValidation();
if (validationPass) {
resp = getPDF();
responseHeaders.setContentType(MediaType.APPLICATION_PDF);
responseHeaders.set("charset", "utf-8");
responseHeaders.setContentLength(resp.length);
responseHeaders.set("Content-disposition", "attachment; filename=response.pdf");
return new ResponseEntity<>(resp,responseHeaders, HttpStatus.OK);
}
else {
// create your json validation eneity here
return new ResponseEntity<>(validation entity, HttpStatus.BAD_REQUEST);
}
} catch (Exception e) {
return new ResponseEntity<>( createExceptionEntity(e) ,HttpStatus.BAD_REQUEST);
}
}
I have a problem while deserialization of HTTP request value into enum with custom com.fasterxml.jackson.databind.JsonDeserializer deserializer implementation:
public class EnvelopeColorJsonDeserializer extends JsonDeserializer<EnvelopeColor> {
#Override
public EnvelopeColor deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String val = p.getValueAsString();
return EnvelopeColor.fromCode(val);
}
}
This is how I convert value into enum:
public static EnvelopeColor fromCode(String code) {
Assert.notNull(code, "code");
for (EnvelopeColor type : values()) {
if (code.equals(type.code)) {
return type;
}
}
throw new RuntimeException("Not supported color: " + code);
}
Endpoint:
#PostMapping("/")
public #ResponseBody
ResponseEntity add(#RequestBody EnvelopeDto envelope) {
// some stuff goes here...
}
Question
Is there some way how to a) check if HTTP request value is valid enum constant value before going to deserialization process or b) how to catch exception in #ControllerAdvice exception handler? (I would like to avoid throwing custom exception in fromCode() method).
You can add an exception handler for class HttpMessageNotReadableException.
This exception is thrown when Spring is not able to deserialize the payload into the DTO.
#ExceptionHandler(org.springframework.http.converter.HttpMessageNotReadableException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
protected #ResponseBody handleIncorrectData(HttpMessageNotReadableException ex,
HttpServletRequest request, HttpServletResponse response){
....
}
Moreover you can define a custom EnumConverter which will give the exact details to user which are the correct enum values.
public class CustomEnumConverter extends EnumConverter {
#Override
public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
try {
return super.unmarshal(reader, context);
} catch (IllegalArgumentException e) {
String inputValue = reader.getValue();
Class contextType = context.getRequiredType();
StringBuilder sb = new StringBuilder();
Object[] enumConstants = contextType.getEnumConstants();
for (Object o : enumConstants) {
sb.append(o + ",");
}
if (sb.length() > 0)
sb.deleteCharAt(sb.length() - 1);
throw new InvalidArgumentException(ErrorCode.INVALID_ARGUMENT, inputValue,
reader.getNodeName(), sb.toString());
}
}
}
for catch exception in #ControllerAdvice exception handler, try this :
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ApplicationExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler({RuntimeException.class})
public #ResponseBody Response<Error> handleIllegalArgumentException(Exception e) {
logger.error("BadRequestException :: ", e);
return new Response<Error>(new Error("BadRequestException", e.getMessage()));
}
}
I have a code like:
public class BigDecimalDeserializer extends JsonDeserializer<BigDecimal> {
private static final Pattern PATTERN = Pattern
.compile("^([1-9]+[0-9]*)((\\.)[0-9]+)?$");
private static final String MESSAGE = "must be a number in format (99 or 99.99)";
private static final String EMPTY_STRING = "";
#Override
public BigDecimal deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException
{
BigDecimal result = null;
JsonNode node = parser.getCodec().readTree(parser);
String text = node.asText();
if (text != null && !text.equals(EMPTY_STRING)) {
Matcher matcher = PATTERN.matcher(text);
if (matcher.matches()) {
result = new BigDecimal(text);
} else {
throw new ApplicationException(MESSAGE);
}
}
return result;
}
}
Nothing happens when this method throws ApplicationException or other type of exception.
Where this exception catches? And why there is no stacktraces in the console?
p.s. im trying to handle this exception in the #ControllerAdvice class (#ExceptionHandler(ApplicationException.class) method)..but nothing happens.
MessageConverter config:
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
converter.setObjectMapper(objectMapper);
converters.add(converter);
super.configureMessageConverters(converters);
}
ContollerAdvice:
#ExceptionHandler(ApplicationException.class)
protected ResponseEntity<Object> handleWrongRequest(
RuntimeException exception, WebRequest request) {
ApplicationException applicationException = (ApplicationException) exception;
Error error = new Error();
error.setField(EMPTY);
error.setMessage(applicationException.getRootMessage());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return handleExceptionInternal(exception, error, headers,
HttpStatus.FORBIDDEN, request);
}
Where Spring handles Jackson exceptions?
I Am using Spring's #ExceptionHandler annotation to catch exceptions in my controllers.
Some requests hold POST data as plain XML string written to the request body, I want to read that data in order to log the exception.
The problem is that when i request the inputstream in the exception handler and try to read from it the stream returns -1 (empty).
The exception handler signature is:
#ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff)
Any thoughts? Is there a way to access the request body?
My controller:
#Controller
#RequestMapping("/user/**")
public class UserController {
static final Logger LOG = LoggerFactory.getLogger(UserController.class);
#Autowired
IUserService userService;
#RequestMapping("/user")
public ModelAndView getCurrent() {
return new ModelAndView("user","response", userService.getCurrent());
}
#RequestMapping("/user/firstLogin")
public ModelAndView firstLogin(HttpSession session) {
userService.logUser(session.getId());
userService.setOriginalAuthority();
return new ModelAndView("user","response", userService.getCurrent());
}
#RequestMapping("/user/login/failure")
public ModelAndView loginFailed() {
LOG.debug("loginFailed()");
Status status = new Status(-1,"Bad login");
return new ModelAndView("/user/login/failure", "response",status);
}
#RequestMapping("/user/login/unauthorized")
public ModelAndView unauthorized() {
LOG.debug("unauthorized()");
Status status = new Status(-1,"Unauthorized.Please login first.");
return new ModelAndView("/user/login/unauthorized","response",status);
}
#RequestMapping("/user/logout/success")
public ModelAndView logoutSuccess() {
LOG.debug("logout()");
Status status = new Status(0,"Successful logout");
return new ModelAndView("/user/logout/success", "response",status);
}
#RequestMapping(value = "/user/{id}", method = RequestMethod.POST)
public ModelAndView create(#RequestBody UserDTO userDTO, #PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.create(userDTO, id));
}
#RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public ModelAndView getUserById(#PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.getUserById(id));
}
#RequestMapping(value = "/user/update/{id}", method = RequestMethod.POST)
public ModelAndView update(#RequestBody UserDTO userDTO, #PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.update(userDTO, id));
}
#RequestMapping(value = "/user/all", method = RequestMethod.GET)
public ModelAndView list() {
return new ModelAndView("user", "response", userService.list());
}
#RequestMapping(value = "/user/allowedAccounts", method = RequestMethod.GET)
public ModelAndView getAllowedAccounts() {
return new ModelAndView("user", "response", userService.getAllowedAccounts());
}
#RequestMapping(value = "/user/changeAccount/{accountId}", method = RequestMethod.GET)
public ModelAndView changeAccount(#PathVariable("accountId") Long accountId) {
Status st = userService.changeAccount(accountId);
if (st.code != -1) {
return getCurrent();
}
else {
return new ModelAndView("user", "response", st);
}
}
/*
#RequestMapping(value = "/user/logout", method = RequestMethod.GET)
public void perLogout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
userService.setOriginalAuthority();
response.sendRedirect("/marketplace/user/logout/spring");
}
*/
#ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff) {
Status st = new Status();
try {
Writer writer = new StringWriter();
byte[] buffer = new byte[1024];
//Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
InputStream reader = request.getInputStream();
int n;
while ((n = reader.read(buffer)) != -1) {
writer.toString();
}
String retval = writer.toString();
retval = "";
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView("profile", "response", st);
}
}
Thank you
I've tried your code and I've found some mistakes in the exception handler, when you read from the InputStream:
Writer writer = new StringWriter();
byte[] buffer = new byte[1024];
//Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
InputStream reader = request.getInputStream();
int n;
while ((n = reader.read(buffer)) != -1) {
writer.toString();
}
String retval = writer.toString();
retval = "";
I've replaced your code with this one:
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String line = "";
StringBuilder stringBuilder = new StringBuilder();
while ( (line=reader.readLine()) != null ) {
stringBuilder.append(line).append("\n");
}
String retval = stringBuilder.toString();
Then I'm able to read from InputStream in the exception handler, it works!
If you can't still read from InputStream, I suggest you to check how you POST xml data to the request body.
You should consider that you can consume the Inputstream only one time per request, so I suggest you to check that there isn't any other call to getInputStream(). If you have to call it two or more times you should write a custom HttpServletRequestWrapper like this to make a copy of the request body, so you can read it more times.
UPDATE
Your comments has helped me to reproduce the issue. You use the annotation #RequestBody, so it's true that you don't call getInputStream(), but Spring invokes it to retrieve the request's body. Have a look at the class org.springframework.web.bind.annotation.support.HandlerMethodInvoker: if you use #RequestBody this class invokes resolveRequestBody method, and so on... finally you can't read anymore the InputStream from your ServletRequest. If you still want to use both #RequestBody and getInputStream() in your own method, you have to wrap the request to a custom HttpServletRequestWrapper to make a copy of the request body, so you can manually read it more times.
This is my wrapper:
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = Logger.getLogger(CustomHttpServletRequestWrapper.class);
private final String body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
logger.error("Error reading the request body...");
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
logger.error("Error closing bufferedReader...");
}
}
}
body = stringBuilder.toString();
}
#Override
public ServletInputStream getInputStream() throws IOException {
final StringReader reader = new StringReader(body);
ServletInputStream inputStream = new ServletInputStream() {
public int read() throws IOException {
return reader.read();
}
};
return inputStream;
}
}
Then you should write a simple Filter to wrap the request:
public class MyFilter implements Filter {
public void init(FilterConfig fc) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new CustomHttpServletRequestWrapper((HttpServletRequest)request), response);
}
public void destroy() {
}
}
Finally, you have to configure your filter in your web.xml:
<filter>
<filter-name>MyFilter</filter-name>
<filter-class>test.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>MyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
You can fire your filter only for controllers that really needs it, so you should change the url-pattern according to your needs.
If you need this feature in only one controller, you can also make a copy of the request body in that controller when you receive it through the #RequestBody annotation.
Recently I faced this issue and solved it slightly differently. With spring boot 1.3.5.RELEASE
The filter was implemented using the Spring class ContentCachingRequestWrapper. This wrapper has a method getContentAsByteArray() which can be invoked multiple times.
import org.springframework.web.util.ContentCachingRequestWrapper;
public class RequestBodyCachingFilter implements Filter {
public void init(FilterConfig fc) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new ContentCachingRequestWrapper((HttpServletRequest)request), response);
}
public void destroy() {
}
}
Added the filter to the chain
#Bean
public RequestBodyCachingFilter requestBodyCachingFilter() {
log.debug("Registering Request Body Caching filter");
return new RequestBodyCachingFilter();
}
In the Exception Handler.
#ControllerAdvice(annotations = RestController.class)
public class GlobalExceptionHandlingControllerAdvice {
private ContentCachingRequestWrapper getUnderlyingCachingRequest(ServletRequest request) {
if (ContentCachingRequestWrapper.class.isAssignableFrom(request.getClass())) {
return (ContentCachingRequestWrapper) request;
}
if (request instanceof ServletRequestWrapper) {
return getUnderlyingCachingRequest(((ServletRequestWrapper)request).getRequest());
}
return null;
}
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler(Throwable.class)
public #ResponseBody Map<String, String> conflict(Throwable exception, HttpServletRequest request) {
ContentCachingRequestWrapper underlyingCachingRequest = getUnderlyingCachingRequest(request);
String body = new String(underlyingCachingRequest.getContentAsByteArray(),Charsets.UTF_8);
....
}
}
I had the same problem and solved it with HttpServletRequestWrapper as described above and it worked great. But then, I found another solution with extending HttpMessageConverter, in my case that was MappingJackson2HttpMessageConverter.
public class CustomJsonHttpMessageConverter extends MappingJackson2HttpMessageConverter{
public static final String REQUEST_BODY_ATTRIBUTE_NAME = "key.to.requestBody";
#Override
public Object read(Type type, Class<?> contextClass, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
final ByteArrayOutputStream writerStream = new ByteArrayOutputStream();
HttpInputMessage message = new HttpInputMessage() {
#Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
#Override
public InputStream getBody() throws IOException {
return new TeeInputStream(inputMessage.getBody(), writerStream);
}
};
RequestContextHolder.getRequestAttributes().setAttribute(REQUEST_BODY_ATTRIBUTE_NAME, writerStream, RequestAttributes.SCOPE_REQUEST);
return super.read(type, contextClass, message);
}
}
com.sun.xml.internal.messaging.saaj.util.TeeInputStream is used.
In spring mvc config
<mvc:annotation-driven >
<mvc:message-converters>
<bean class="com.company.remote.rest.util.CustomJsonHttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
In #ExceptionHandler method
#ExceptionHandler(Exception.class)
public ResponseEntity<RestError> handleException(Exception e, HttpServletRequest httpRequest) {
RestError error = new RestError();
error.setErrorCode(ErrorCodes.UNKNOWN_ERROR.getErrorCode());
error.setDescription(ErrorCodes.UNKNOWN_ERROR.getDescription());
error.setDescription(e.getMessage());
logRestException(httpRequest, e);
ResponseEntity<RestError> responseEntity = new ResponseEntity<RestError>(error,HttpStatus.INTERNAL_SERVER_ERROR);
return responseEntity;
}
private void logRestException(HttpServletRequest request, Exception ex) {
StringWriter sb = new StringWriter();
sb.append("Rest Error \n");
sb.append("\nRequest Path");
sb.append("\n----------------------------------------------------------------\n");
sb.append(request.getRequestURL());
sb.append("\n----------------------------------------------------------------\n");
Object requestBody = request.getAttribute(CustomJsonHttpMessageConverter.REQUEST_BODY_ATTRIBUTE_NAME);
if(requestBody != null) {
sb.append("\nRequest Body\n");
sb.append("----------------------------------------------------------------\n");
sb.append(requestBody.toString());
sb.append("\n----------------------------------------------------------------\n");
}
LOG.error(sb.toString());
}
I hope it helps :)