I am trying to log (WARNING and ERROR with slf4j) every 4xx and 5xx and the request made from the client including headers and the payload.
I also want to log the response which my application responds with, no matter if it's an exception message generated by Spring itself, or a custom message that I've returned from my controller.
These are my controllers that I am using for testing:
#RequestMapping(path = "/throw", method = RequestMethod.GET)
public String Fail(){
String nul = null;
nul.toCharArray();
return "Hello World";
}
#RequestMapping(path = "/null", method = RequestMethod.GET)
public ResponseEntity Custom() {
return ResponseEntity.notFound().build();
}
I have tried following methods:
ControllerAdvice
Found out that this is only for handling exceptions. I need to handle any 4xx and 5xx response returned from my controller.
Using filter
By using CommonsRequestLoggingFilter I can log the request, including the payload. However, this does not log when an exception is thrown (which is handled by Spring).
Using interceptor
With interceptor I should be able to intercept both incoming and outgoing data with the following code:
private static final Logger log = LoggerFactory.getLogger(RequestInterceptor.class);
class RequestLog {
public String requestMethod;
public String requestUri;
public String requestPayload;
public String handlerName;
public String requestParams;
RequestLog(String requestMethod, String requestUri, String requestPayload, String handlerName, Enumeration<String> requestParams) {
this.requestMethod = requestMethod;
this.requestUri = requestUri;
this.requestPayload = requestPayload;
this.handlerName = handlerName;
StringBuilder stringBuilder = new StringBuilder();
while (requestParams.hasMoreElements()) {
stringBuilder
.append(";")
.append(requestParams.nextElement());
}
this.requestParams = stringBuilder.toString();
}
}
class ResponseLog {
public int responseStatus;
public String responsePayload;
public ResponseLog(int responseStatus, String responsePayload) {
this.responseStatus = responseStatus;
this.responsePayload = responsePayload;
}
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestUri = request.getRequestURI();
String requestPayload = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
Enumeration<String> requestParams = request.getParameterNames();
String requestMethod = request.getMethod();
String handlerName = handler.toString();
RequestLog requestLog = new RequestLog(requestMethod, requestUri, requestPayload, handlerName, requestParams);
String serialized = new ObjectMapper().writeValueAsString(requestLog);
log.info("Incoming request:" + serialized);
return super.preHandle(request, response, handler);
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws IOException {
int responseStatus = response.getStatus();
boolean is4xx = String.valueOf(responseStatus).startsWith("4");
boolean is5xx = String.valueOf(responseStatus).startsWith("5");
if (is4xx || is5xx || ex != null) {
String responseBody = getResponseBody(response);
ResponseLog responseLog = new ResponseLog(responseStatus, responseBody);
String serialized = new ObjectMapper().writeValueAsString(responseLog);
log.warn("Response to last request:" + serialized);
}
}
private String getResponseBody(HttpServletResponse response) throws UnsupportedEncodingException {
String responsePayload = "";
ContentCachingResponseWrapper wrappedRequest = new ContentCachingResponseWrapper(response);
byte[] responseBuffer = wrappedRequest.getContentAsByteArray();
if (responseBuffer.length > 0) {
responsePayload = new String(responseBuffer, 0, responseBuffer.length, wrappedRequest.getCharacterEncoding());
}
return responsePayload;
}
When requesting /throw I get following log from the interceptor:
2017-12-11 21:40:15.619 INFO 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor : Incoming request:{"requestMethod":"GET","requestUri":"/throw","requestPayload":"","handlerName":"public java.lang.String com.example.demo.controllers.IndexController.Fail()","requestParams":""}
2017-12-11 21:40:15.635 WARN 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor : Response to last request:{"responseStatus":200,"responsePayload":""}
*stackTrace because of nullpointer...*
2017-12-11 21:40:15.654 INFO 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor : Incoming request:{"requestMethod":"GET","requestUri":"/error","requestPayload":"","handlerName":"public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)","requestParams":""}
2017-12-11 21:40:15.675 WARN 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor : Response to last request:{"responseStatus":500,"responsePayload":""}
With request to /null:
2017-12-11 21:48:14.815 INFO 12220 --- [nio-8080-exec-3] c.e.demo.interceptor.RequestInterceptor : Incoming request:{"requestMethod":"GET","requestUri":"/null","requestPayload":"","handlerName":"public org.springframework.http.ResponseEntity com.example.demo.controllers.IndexController.Custom()","requestParams":""}
2017-12-11 21:48:14.817 WARN 12220 --- [nio-8080-exec-3] c.e.demo.interceptor.RequestInterceptor : Response to last request:{"responseStatus":404,"responsePayload":""}
There are two issues here:
Response body is always null (even though the client receives error response from Spring). How can I fix this?
Seems like Spring is redirecting to /error when an exception occurs
TL;DR: I need to log the request to my Spring application and the response (including the payload) to client. How can I solve this problem?
Possible solution using both Filter and ControllerAdvice:
Filter:
#Component
public class LogFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(LogFilter.class);
private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 1000;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
logRequest(request);
filterChain.doFilter(requestWrapper, responseWrapper);
logResponse(responseWrapper);
}
private void logResponse(ContentCachingResponseWrapper responseWrapper) {
String body = "None";
byte[] buf = responseWrapper.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, DEFAULT_MAX_PAYLOAD_LENGTH);
try {
body = new String(buf, 0, length, responseWrapper.getCharacterEncoding());
responseWrapper.copyBodyToResponse();
} catch (IOException e) {
e.printStackTrace();
}
}
int responseStatus = responseWrapper.getStatusCode();
boolean is4xx = String.valueOf(responseStatus).startsWith("4");
boolean is5xx = String.valueOf(responseStatus).startsWith("5");
if(is4xx) logger.warn("Response: statusCode: {}, body: {}", responseStatus, body);
else if (is5xx) logger.error("Response: statusCode: {}, body: {}", responseStatus, body);
}
private void logRequest(HttpServletRequest request) {
String body = "None";
try {
body = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
} catch (IOException e) {
e.printStackTrace();
}
logger.warn("Incoming request {}: {}", request.getRequestURI() , body);
}
}
ControllerAdvice:
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
CustomException customException = new CustomException(NOT_FOUND, ex.getMessage(), ex.getLocalizedMessage(), ex);
ex.printStackTrace();
return new ResponseEntity<>(customException, customException.getStatus());
}
#ResponseBody
#ExceptionHandler(Exception.class)
protected ResponseEntity<Object> handleSpringExceptions(HttpServletRequest request, Exception ex) {
CustomException customException = new CustomException(INTERNAL_SERVER_ERROR, ex.getMessage(), ex.getLocalizedMessage(), ex);
ex.printStackTrace();
return new ResponseEntity<>(customException, customException.getStatus());
}
#Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
CustomException customException = new CustomException(INTERNAL_SERVER_ERROR, ex.getMessage(),ex.getLocalizedMessage(), ex);
ex.printStackTrace();
return new ResponseEntity<>(customException, customException.getStatus());
}
}
The filter can log any request and response that we have handled inside the controller, however the response payload seems to be always empty when an exception is thrown (because Spring handles it and creates a custom message). I am not sure how this works under the hood, but I managed to overcome this problem by using ControllerAdvice in addition (the response is passed through the filter...). Now I can log any 4xx and 5xx properly. If someone has better solution I will accept that instead.
Note: CustomException is just a class with fields that I want to send to the client.
public class CustomException{
public String timestamp;
public HttpStatus status;
public String exceptionMessage;
public String exceptionType;
public String messageEn;
public String messageNo;
...
}
Related
From client side am passing an AES encrypted String with Content Type text/plain.
The AES encrypted String is Decrypted before reaching the controller through a Filter.
CustomEncryptedFilter
#Component
#Order(0)
public class CustomEncryptedFilter implements Filter {
private static final Logger logger = LogManager.getLogger(CustomEncryptedFilter.class.getName());
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("************** Encryption Filter - START ***********************");
String encryptedString = IOUtils.toString(request.getInputStream());
if (encryptedString != null && encryptedString.length() > 0) {
byte[] decryptedString = new AESEncrytion().decrypt(encryptedString).getBytes();
if (request instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
CustomHttpServletRequestWrapper requestWrapper
= new CustomHttpServletRequestWrapper(httpServletRequest,decryptedString);
logger.info("Content Type: {}", requestWrapper.getContentType());
logger.info("Request Body: {}", IOUtils.toString(requestWrapper.getInputStream()));
chain.doFilter(requestWrapper, response);
} else {
chain.doFilter(request, response);
}
} else {
logger.info("Request is Invalid or Empty");
chain.doFilter(request, response);
}
}
}
Here I will getting the current request body which is an AES encrypted String
then am decrypting it to convert into a String.
encrypted String - Ijwmn5sZ5HqoUPb15c5idjxetqmC8Sln6+d2BPaYzxA=
Original String - {"username":"thivanka"}
After getting the decrypted String (Json object) i am appending it to the request body
by extending HttpServletRequestWrapper
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = LogManager.getLogger(CustomHttpServletRequestWrapper.class.getName());
private ByteArrayInputStream requestBody;
public CustomHttpServletRequestWrapper(HttpServletRequest request, byte[] decryptedString) {
super(request);
try {
requestBody = new ByteArrayInputStream(decryptedString);
} catch (Exception e) {
logger.error(e);
e.printStackTrace();
}
}
#Override
public String getHeader(String headerName) {
String headerValue = super.getHeader(headerName);
if ("Accept".equalsIgnoreCase(headerName)) {
return headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
} else if ("Content-Type".equalsIgnoreCase(headerName)) {
return headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
}
return headerValue;
}
#SuppressWarnings("unchecked")
#Override
public Enumeration getHeaderNames() {
HttpServletRequest request = (HttpServletRequest) getRequest();
List list = new ArrayList();
Enumeration e = request.getHeaderNames();
while (e.hasMoreElements()) {
String headerName = (String) e.nextElement();
String headerValue = request.getHeader(headerName);
if ("Accept".equalsIgnoreCase(headerName)) {
headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
} else if ("Content-Type".equalsIgnoreCase(headerName)) {
headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
}
list.add(headerName);
}
return Collections.enumeration(list);
}
#SuppressWarnings("unchecked")
#Override
public Enumeration getHeaders(final String headerName) {
HttpServletRequest request = (HttpServletRequest) getRequest();
List list = new ArrayList();
Enumeration e = request.getHeaders(headerName);
while (e.hasMoreElements()) {
String header = e.nextElement().toString();
if (header.equalsIgnoreCase(MediaType.TEXT_PLAIN_VALUE)) {
header = MediaType.APPLICATION_JSON_VALUE;
}
list.add(header);
}
return Collections.enumeration(list);
}
#Override
public String getContentType() {
String contentTypeValue = super.getContentType();
if (MediaType.TEXT_PLAIN_VALUE.equalsIgnoreCase(contentTypeValue)) {
return MediaType.APPLICATION_JSON_VALUE;
}
return contentTypeValue;
}
#Override
public BufferedReader getReader() throws UnsupportedEncodingException {
return new BufferedReader(new InputStreamReader(requestBody, "UTF-8"));
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
#Override
public int read() {
return requestBody.read();
}
#Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
#Override
public void setReadListener(ReadListener listener) {
// TODO Auto-generated method stub
}
};
}
}
Apart from adding the new request body am also changing the MediaType from text/plain
to application/json in order for my #RequestBody annotation to pick up the media type and
perform object conversion.
Here's my Controller
#CrossOrigin(origins = "*", allowedHeaders = "*")
#RestController
#RequestMapping("/api/mobc")
public class HomeController {
private static final Logger logger = LogManager.getLogger(HomeController.class.getName());
#RequestMapping(value="/hello", method=RequestMethod.POST,consumes="application/json", produces="application/json")
public ResponseEntity<?> Message(#RequestBody LoginForm loginForm,HttpServletRequest request) {
logger.info("In Home Controller");
logger.info("Content Type: {}", request.getContentType());
return ResponseEntity.status(HttpStatus.OK).body(loginForm);
}
}
LoginForm Object (I removed the Getters/Setters for readability)
public class LoginForm {
private String username;
private String password;
}
Unfortunately am getting the error. What am i doing wrong here.
ExceptionHandlerExceptionResolver - Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
Possible issue
I suppose that IOUtils.toString(InputStream stream) reads all bytes from the InputStream. But InputStream could be read only once.
Your logging statement
logger.info("Request Body: {}", IOUtils.toString(requestWrapper.getInputStream()));
Reads an InputStream, so Spring can't read it a second time. Try replacing IOUtils.toString(requestWrapper.getInputStream()) with new String(encryptedString, Charset.defaultCharset()).
Other implementation proposal
You can implement custom RequestBodyAdvice which will decrypt the message and change headers if needed.
As from Spring's JavaDoc:
Implementations of this contract may be registered directly with the RequestMappingHandlerAdapter or more likely annotated with #ControllerAdvice in which case they are auto-detected.
Here is an example implementation of advice that changes the first byte of a message to { and last byte to }. Your implementation can modify the message decrypting it.
#ControllerAdvice
class CustomRequestBodyAdvice extends RequestBodyAdviceAdapter {
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
try (InputStream inputStream = inputMessage.getBody()) {
byte[] bytes = inputStream.readAllBytes();
bytes[0] = 0x7b; // 0x7b = '{'
bytes[bytes.length - 1] = 0x7d; // 0x7d = '}'
return new CustomMessage(new ByteArrayInputStream(bytes), inputMessage.getHeaders());
}
}
}
class CustomMessage implements HttpInputMessage {
private final InputStream body;
private final HttpHeaders httpHeaders;
public CustomMessage(InputStream body, HttpHeaders httpHeaders) {
this.body = body;
this.httpHeaders = httpHeaders;
}
#Override
public InputStream getBody() throws IOException {
return this.body;
}
#Override
public HttpHeaders getHeaders() {
return this.httpHeaders;
}
}
Also, there is supports method that returns whether this RequestBodyAdvice should be called. In this example this method always returns true, but you can create custom annotation and check for its existence.
// custom annotation
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#interface AesEncrypted {}
// class: CustomRequestBodyAdvice
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasParameterAnnotation(AesEncrypted.class);
}
// controller
#PostMapping("one")
String getDecrypted(#AesEncrypted #RequestBody Data data) {
return data.value;
}
If anyone is struggling with this then the answer is to move to a ContentCachingRequestWrapper. Other approach would be to use the aspect oriented variation suggested by #geobreze which solves the same question.
I just had to modify my HttpServletRequestWrapper to facilitate the change.
Refs -> https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times
This class caches the request body by consuming the InputStream. If we
read the InputStream in one of the filters, then other subsequent
filters in the filter chain can't read it anymore. Because of this
limitation, this class is not suitable in all situations.
Hi I am pretty new to Spring and Angular.I am building a spring java server and an angular client. Basically , i want the Client to be able to catch the exception throw out from the server. I defined a CustomExeption.java class and have an CustomRestExcepotionHandler.java on serverside. Right now I am not sure where should i throw out the exception in the server for the client to catch.
I was following the tutorial : https://www.baeldung.com/global-error-handler-in-a-spring-rest-api
Now it returns me with 500 Internal Server Error error message to the client side in HttpErrorResponse.
I want it to return my customexception message. Could someone help me to see if server side code has any problem. why did the HttpErrorResponse not catching the CustomException throw out? Thanks!
public class ApiError {
private HttpStatus status;
private String message;
private List<String> errors;
public ApiError(HttpStatus status, String message, List<String> errors) {
super();
this.status = status;
this.message = message;
this.errors = errors;
}
public ApiError(HttpStatus status, String message, String error) {
super();
this.status = status;
this.message = message;
errors = Arrays.asList(error);
}
public HttpStatus getStatus() {
// TODO Auto-generated method stub
return status;
}
public String getMessage() {
// TODO Auto-generated method stub
return message;
}
}
---
--------------------ExceptionHandler
#ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
HttpStatus status, WebRequest request) {
ApiError apiError =
new ApiError(status, ex.getMessage(), ex.getMessage());
return handleExceptionInternal(
ex, apiError, headers, apiError.getStatus(), request);
}
protected ResponseEntity<Object> handleResponseStatusException(ResponseStatusException ex,Object body, HttpHeaders headers,
HttpStatus status, WebRequest request ){
ApiError apiError =
new ApiError(status, ex.getMessage(), ex.getMessage());
return handleExceptionInternal(
ex, apiError, headers, apiError.getStatus(), request);
}
}
public ResponseEntity<AtlasJWT> signInUser(String userName, String password) {String userId = "(uid=" + userName + ")";
if (ldapTemplate.authenticate("", userId, password)) {
log.info("ldapTemplate.authenticate returned true");
Optional<AtlasUser> optLoggedInUser = userRepository.findByUsername(userName);
AtlasJWT atlasJwtToken = jwtTokenProvider.createAtlasJwtToken(optLoggedInUser.get());
if (optLoggedInUser.isPresent()) {
log.info("Atlas JWT: {}", atlasJwtToken);
return new ResponseEntity<AtlasJWT>(atlasJwtToken, HttpStatus.OK);
} else {
//ApiError error = new ApiError(HttpStatus.BAD_REQUEST,"No such User found in the Atlas Database","No such User found in the Atlas Database");
throw new CustomException("No such User found in the Atlas Database",HttpStatus.FORBIDDEN);
}
} else {
//ApiError error = new ApiError(HttpStatus.FORBIDDEN,"Invalid username/password supplied","Invalid username/password supplied");
throw new CustomException("Invalid username/password supplied", HttpStatus.FORBIDDEN);
}
}
my Client side login Component is like below:
login(username: string, password: string) {
console.log('Inside AuthenticationService. Username: ', username);
// const body = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&grant_type=password`;
const body = {
'username': username,
'password': password
};
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
console.log('Invoking server authentication for username', username);
return this.http.post<AtlasJWT>('/auth/api/signin', body, httpOptions).pipe(catchError(this.handleError));
}
private handleError(err: HttpErrorResponse) {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
let errorMessage = '';
if (err.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
errorMessage = err.message;
// console.log(err);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
errorMessage = `Server returned code: ${err.status}, error message is: ${err.message}`;
console.log(err);
}
console.error(errorMessage);
return throwError(errorMessage);
}
I feel like this helped. Added the annotation of #ResponseBody And #ResponseStatus.
And i also try this code , added in my controller class.both working
#ExceptionHandler(CustomException.class)
public HttpEntity<String> exceptionHandler(CustomException custEx ) {
return new HttpEntity<String>(custEx.getMessage()) ;
}
I'm using CommonsRequestLoggingFilter to log any incoming requests on my #RestController.
#RestController
public class MyController {
#PostMapping("/")
public MyRsp ping() {
...
return myRsp;
}
}
The users will send POST JSON requests, which are already logged using:
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeClientInfo(true);
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
return filter;
}
Question: how can I achieve the same for the JSON Response that I sent back to the user?
I don't know why spring offers a rest logger that uses ContentCachingRequestWrapper, but does not offer response logging. Because it can be implemented quite similar to the req logging, as follows:
public class MyFilter extends CommonsRequestLoggingFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
//same mechanism as for request caching in superclass
HttpServletResponse responseToUse = response;
if (isIncludePayload() && !isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) {
responseToUse = new ContentCachingResponseWrapper(response);
}
//outgoing request is logged in superclass
super.doFilterInternal(request, responseToUse, filterChain);
//log incoming response
String rsp = getResponseMessage(responseToUse);
LOGGER.info(rsp);
}
//equivalent to super.createMessage() for request logging
private String getResponseMessage(HttpServletResponse rsp) {
StringBuilder msg = new StringBuilder();
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(request, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, getMaxPayloadLength());
String payload;
try {
payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
}
catch (UnsupportedEncodingException ex) {
payload = "[unknown]";
}
msg.append(";payload=").append(payload);
}
}
}
}
#Pointcut("#annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestMapping() {}
#Pointcut("within(path.to your.controller.package.*)")
public void myController() {}
#Around("requestMapping() || myController()")
public MyRsp logAround(ProceedingJoinPoint joinPoint) throws Throwable {
joinPoint.getArgs()// This will give you arguments if any being passed to your controller.
...............
MyRsp myRsp = (MyRsp) joinPoint.proceed();
...............
return myRsp;
}
I have this javax.servlet.Filter to check whether client is allowed to access API REST resource.
#Component
public class AuthorizationRequestFilter implements Filter {
public static final String AUTHORIZATION_TOKEN = "X-Access-Token";
#Autowired
#Qualifier("loginService")
private ILoginService loginService;
private void throwUnauthorized(ServletResponse res) throws IOException {
HttpServletResponse response = (HttpServletResponse) res;
response.reset();
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
private void throwForbidden(ServletResponse res) throws IOException {
HttpServletResponse response = (HttpServletResponse) res;
response.reset();
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String accessToken = request.getHeader(AUTHORIZATION_TOKEN);
if (StringUtils.isEmpty(accessToken)) {
throwUnauthorized(res);
} else {
AccountLoginData account = loginService.find(accessToken);
if (account == null) {
throwForbidden(res);
}
}
chain.doFilter(req, res);
}
#Override
public void destroy() {
}
#Override
public void init(FilterConfig arg0) throws ServletException {
}
}
it works but I would like to in these two throw*() methods write to the client JSON with appropriate information. In another part of this application I use these response message objects to inform client what happened.
For example, when record has not been found:
public class NotFoundResponseMessage extends ResponseMessage {
public NotFoundResponseMessage(String message) {
super(HttpStatus.NOT_FOUND, 1, message);
}
}
and
public class ResponseMessage {
private int status;
private int code;
private String message;
private String reason;
public ResponseMessage(int status, int code, String message, String reason) {
Assert.notNull(reason, "Reason must not be null.");
Assert.isTrue(status > 0, "Status must not be empty.");
this.status = status;
this.code = code;
this.message = message;
this.reason = reason;
}
}
My Question
I would like to return JSON with serialized objects (UnauthorizedResponseMessage and ForbiddenResponseMessage) in my javax.servlet.Filter authorization / authentication filter. I use Spring Boot and Jackson library.
How can I manually serialize ResponseMessage into its JSON representation?
How can I write out this JSON back to the client in my filter class?
Edit 1:
private void throwUnauthorized(ServletResponse res) throws IOException {
HttpServletResponse response = (HttpServletResponse) res;
response.reset();
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"foo\":\"boo\"}");
}
Now I can write out JSON but HTTP 500 is returned, because:
java.lang.IllegalStateException: getWriter() has already been called for this response
at org.apache.catalina.connector.Response.getOutputStream(Response.java:544)
Using Jackson convert Object to JSON, the following is an example
ObjectMapper mapper = new ObjectMapper();
String Json = mapper.writeValueAsString(object);
I had the same problem, the complete solution is the following:
try {
restResponse = service.validate(httpReq);
} catch (ForbiddenException e) {
ObjectMapper mapper = new ObjectMapper();
ResponseObject object = new ResponseObject();
object.setStatus(HttpServletResponse.SC_FORBIDDEN);
object.setMessage(e.getMessage());
object.setError("Forbidden");
object.setTimestamp(String.valueOf(new Date().getTime()));
HttpServletResponse httpResp = (HttpServletResponse) response;
httpResp.reset();
httpResp.setHeader("Content-Type","application/json;charset=UTF-8");
httpResp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
String json = mapper.writeValueAsString(object);
response.getWriter().write(json);
return;
}
and the result is:
Just throw your exceptions from the filter and annotate the thrown exception with #ResponseStatus. This way it automatically gets translated to the given http error code. (you can also define the error message)
Code example:
#ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Error while trying to add the feed.")
public class AddFeedException extends Exception {
private static final long serialVersionUID = 290724913968202592L;
public AddFeedException(Throwable throwable) {
super(throwable);
}
}
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 :)