How to get ServerHttpRequest/ServerHttpResponse body to string - java

Want to implement a proxy on top of Spring API Gateway to log requests/responses.
I defined my own filters for incoming request and outcoming responses.
REQUEST GATEWAY FILTER:
public class RequestGatewayFilter extends AbstractGatewayFilterFactory<RequestGatewayFilter.Config> {
private static final Logger logger = LogManager.getLogger(RequestGatewayFilter.class);
public RequestGatewayFilter() {
super(Config.class);
}
#Autowired
CustomProxyLogger customLogger;
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest.Builder builder = exchange.getRequest().mutate()
.header(PR_CORRELATION_ID, UUID.randomUUID().toString());
ServerHttpRequest request = builder.build();
customLogger.logRequest(logger, request);
return chain.filter(exchange.mutate().request(request).build());
};
}
}
RESPONSE GATEWAY FILTER:
public class ResponseGatewayFilter extends AbstractGatewayFilterFactory<ResponseGatewayFilter.Config> {
private static final Logger logger = LogManager.getLogger(ResponseGatewayFilter.class);
public ResponseGatewayFilter() {
super(Config.class);
}
#Autowired
CustomProxyLogger customLogger;
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
customLogger.logResponse(logger, exchange);
}));
};
}
I have to log request and response body. I tried several ways for example as explained on How to correctly read Flux<DataBuffer> and convert it to a single inputStream
In this case the map function did not execute anytime.
Also tried casting ServerHttpRequest to HttpServletRequest in order to get body from there but this throws Cast Exception.
None worked....
Any ideas or possible approaches to solve this 'get body' problem ?

Simply create another one Filter and log all requests, responses.
#Component
public class RequestResponseLoggingFilter implements Filter {
...
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
LOG.info(request);
chain.doFilter(request, response);
LOG.info(response);
}
}

Related

How to get response from HttpServletResponse and save to database in Java via interceptor

I want to save request and response in String format to database when endpoint /v2 is called.
This implementation workes fine and save is correct but if i call to v1 endpoint, is muted, response is null, just 200 OK in Postman. Normally it response with json {id="123456789"} Whats wrong have i in this implementation that endpoint v1 doesnt work ? If i delete CachingRequestBodyFilter class, v1 works fine but when v2 is called, nothing saved to database.
v1 endpoint is for xml and v2 endpoint is for json format.
Meybe is better way to save request and response to db via interceptors?
#AllArgsConstructor
#Component
#Data
public class RequestInterceptor implements HandlerInterceptor {
private final RequestService requestService;
#Override
public void afterCompletion(#NonNull HttpServletRequest request, #NonNull HttpServletResponse response, #NonNull Object handler, Exception ex){
requestService.saveResponse(request, response, ex);
}
}
public void saveResponse(HttpServletRequest request, HttpServletResponse response, Exception ex) {
try {
String requestBody = getRequestAsString(request);
String responseBody = getResponseAsString(response);
buildMessages(requestBody, responseBody, ex);
} catch (IOException e) {
e.printStackTrace();
}
}
private String getRequestAsString(HttpServletRequest request) throws IOException {
ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
return new String(requestWrapper.getContentAsByteArray(), requestWrapper.getCharacterEncoding());
}
private String getResponseAsString(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response;
byte[] responseArray = responseWrapper.getContentAsByteArray();
String characterEncoding = responseWrapper.getCharacterEncoding();
responseWrapper.copyBodyToResponse();
return new String(responseArray, characterEncoding);
}
method buildMessages() is just a builder object which i want to save to db.
filter class:
#Component
#WebFilter(filterName = "CachingRequestBodyFilter", urlPatterns = {"/v2/*"})
public class CachingRequestBodyFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
HttpServletResponse response = (HttpServletResponse) res;
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
}
i want to save my request and response to db when endpoint v2 is called and when v1 is called response is not null;

Update request headers before goind to controller

I can't update a header inside my interceptor before it gets to my controller, through the interceptor I would like to modify an already present header
public class MyInterceptor implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse res = (HttpServletResponse) servletResponse;
//something
//myHeaders is still present when send a request
req.setAttribute("myHeaders","someValue");
chain.doFilter(req, rest);
}
}
So that inside the controller I can get the modified header:
#RestController
#RequestMapping("/")
public class FooClass{
#Autowired
private Service service;
#GetMapping("/foo")
public ResponseEntity<Void> fooApi(
#RequestHeader(value = "myHeaders") String myHeaders,
) {
service.doSomething(myHeaders);
return ResponseEntity.ok().build();
}
}
How could I do? I tried to do some research but failed.
In your Filter you can create an anonymous subclass of HttpServletRequestWrapper, override the method public String getHeader(String name) so that it returns a specific value for the header name you care about (and delegates to super.getHeader(String) if it's not the header name you care about).
Something like this:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(servletRequest) {
#Override
public String getHeader(String name) {
if ("myHeader".equalsIgnoreCase(name)) {
return "Some value";
}
return super.getHeader(name);
}
};
chain.doFilter(requestWrapper, response);
}

How can I modify the object of a post call in filter. Spring boot

I have a filter in my application
#Component
#Order(2)
public class RequestResponseLoggingFilter implements Filter {
#Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// SET VALUE OF OBJECT
}
// other methods
}
I have a Restcall which uses a class.
#RequestMapping
Class Test{
#PostMapping("/test")
public void postEntry(#Valid #RequestBody Testing testing){
}
}
Class Testing{
#NotNull(message="ERROR")
String id;
....
}
I get the id in my filter and I would like to set the id of Testing class in my Filter. Is this possible?
You can use MockHttpServletRequest something like this
#Test
public void testAddEventWithWebAuthenticationDetails() {
HttpSession session = new MockHttpSession(null, "test-session-id");
MockHttpServletRequest request = new MockHttpServletRequest();
request.setSession(session);
request.setRemoteAddr("1.2.3.4");
WebAuthenticationDetails details = new WebAuthenticationDetails(request);
Map<String, Object> data = new HashMap<>();
data.put("test-key", details);
AuditEvent event = new AuditEvent("test-user", "test-type", data);
customAuditEventRepository.add(event);
List<PersistentAuditEvent> persistentAuditEvents = persistenceAuditEventRepository.findAll();
assertThat(persistentAuditEvents).hasSize(1);
PersistentAuditEvent persistentAuditEvent = persistentAuditEvents.get(0);
assertThat(persistentAuditEvent.getData().get("remoteAddress")).isEqualTo("1.2.3.4");
assertThat(persistentAuditEvent.getData().get("sessionId")).isEqualTo("test-session-id");
}
More examples here
or
if you want to do it filter way
few Points Before that
Request body can be read only once.
If you read the body in a filter, the target servlet will not be able to re-read it and this will also cause IllegalStateException.
You will need ServletRequestWrapper or its child: HttpServletRequestWrapper so that you can read HTTP request body and then the servlet can still read it later.
Workflow will be
The only way would be for you to consume the entire input stream yourself in the filter.
Take what you want from it, and then create a new InputStream for the content you read.
Put that InputStream in to a ServletRequestWrapper (or HttpServletRequestWrapper).
// Sample Wrapper class where you can read body and modify body content
public class SampleHttpServletRequest
extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;
public SampleHttpServletRequest(HttpServletRequest request) {
super(request);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null)
cacheInputStream();
return new CachedServletInputStream();
}
#Override
public BufferedReader getReader() throws IOException{
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private void cacheInputStream() throws IOException {
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
#Override
public int read() throws IOException {
return input.read();
}
}
}
Filter class
public class MyFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
/* wrap the request in order to read the inputstream multiple times */
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
doMyThing(multiReadRequest.getInputStream());
chain.doFilter(multiReadRequest, response);
}
}
Refer these post for more detail
Http Servlet request lose params from POST body after read it once
HttpServletRequestWrapper, example implementation for setReadListener / isFinished / isReady?

Catch httpServeletRequest in loadUserDetails method

I have a customized spring AuthenticationProvider class but try to intercept the HTTPServletRequest and HTTPServletResponse within the loadUserDetails method.
#Component("darnGoodAuthenticaionProvider")
public class DarnGoodAuthenticaionProvider
extends HandlerInterceptorAdapter
implements AuthenticationUserDetailsService {
private HttpServletRequest request;
private HttpServletResponse response;
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
this.request = request;
this.response = response;
// we don't want anything falling here
return true;
}
#Override
public UserDetails loadUserDetails(Authentication token)throws
UsernameNotFoundException{
.......
}
}
I know the preHandler method from HandlerIntercepterAdapter is capable to the job but how can I be sure that the preHandler method is called prior to loadUserDetails, so that I can get the request and response prepared?
Thanks
On a servlet container, each request will be handled from the moment the request is received until the response is returned by only one thread (request == current thread).
So it's a matter of putting a servlet filter BEFORE the spring security filter chain (with the filter-mapping element above the filter-mapping of spring security), and storing the request and response in the thread using a ThreadLocal variable - see also this answer.
Then on the DarnGoodAuthenticaionProvider access the request using a static method RequestResponseHolder.getRequest().
web.xml config:
<filter>
<filter-name>saveRequestResponseFilter</filter-name>
<filter-class>sample.save.request.filter.SaveRequestResponseFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>saveRequestResponseFilter</filter-name>
<url-pattern>/mobilews/*</url-pattern>
</filter-mapping>
Filter to save the request response in the thread:
public class SaveRequestResponseFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
RequestResponseHolder.setRequestResponse(req,resp);
try {
chain.doFilter(request, response);
}
finally {
RequestResponseHolder.clear();
}
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
...
}
#Override
public void destroy() {
...
}
}
Request/Response holder:
public class RequestResponseHolder {
private static ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<HttpServletRequest>();
private static ThreadLocal<HttpServletResponse> responseHolder = new ThreadLocal<HttpServletResponse>();
public static void setRequestResponse(HttpServletRequest request, HttpServletResponse response) {
requestHolder.set(request);
responseHolder.set(response);
}
public static HttpServletRequest getServletRequest(){
return requestHolder.get();
}
public static HttpServletResponse getServletResponse() {
return responseHolder.get();
}
public static void clear() {
requestHolder.remove();
responseHolder.remove();
}
}
Obtaining the request from DarnGoodAuthenticaionProvider:
HttpServletRequest req = RequestResponseHolder.getServletRequest();

How can I access a request that has been wrapped in a custom class from within my Dropwizard Resource?

I am using Dropwizard and I would like to wrap a request object from within a filter, and gain access to that instance from my resource class
e.g. do the following
Filter.java
public class ServiceRequestExtractionFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
ProxyResourceRequest requestWrapper = new RequestWrapper(httpRequest, svcRequestData);
chain.doFilter(requestWrapper, response);
}
}
...
Resource.java
#Path("/test")
#Produces(MediaType.APPLICATION_JSON)
public class DemoResource {
#GET
#Timed
public Response get(#Context UriInfo uriInfo, #Context RequestWrapper request) {
...
this doesn't work nor do I have any reason to expect it to, but it seemed like a good way to explain what I was trying to do.
Try this ContainerFilter documented here: http://dropwizard.io/manual/core.html#jersey-filters
public class DateNotSpecifiedFilter implements ContainerRequestFilter {
#Context ExtendedUriInfo extendedUriInfo;
#Override
public ContainerRequest filter(ContainerRequest request) {
boolean methodNeedsDateHeader = extendedUriInfo.getMatchedMethod().isAnnotationPresent(DateRequired.class);
String dateHeader = request.getHeaderValue(HttpHeaders.DATE);
if (methodNeedsDateHeader && dateHeader == null) {
Exception cause = new IllegalArgumentException("Date Header was not specified");
throw new WebApplicationException(cause, Response.Status.BAD_REQUEST);
} else {
return request;
}
}
}
Just add this in you application run():
environment.jersey().getResourceConfig().getContainerRequestFilters().add(new DateNotSpecifiedFilter());

Categories