If you're trying to log all incoming HTTP requests in Spring Boot as curl commands, along with their responses, here's a clean solution that worked well for me. It uses a OncePerRequestFilter. Solutions that uses DispatcherServlet may not be ideal for logging raw request/response bodies because by the time it’s reached, the body has often already been consumed. Also the request and response should be wrapped using spring's caching wrappers ContentCachingRequestWrapper and ContentCachingResponseWrapper . Otherwise, you'll encounter an error java.lang.IllegalStateException: getReader() has already been called for this request For more details on this IllegalStateException Read: https://www.baeldung.com/java-servletrequest-illegalstateexception
Note: For multipart requests the solution logs the curl with placeholders for the file parts.
Step 1: Register the Filter Bean
@Configuration
public class HttpFilterConfig {
@Bean
public FilterRegistrationBean<HttpRequestLoggingFilter> loggingFilter() {
FilterRegistrationBean<HttpRequestLoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new HttpRequestLoggingFilter());
registrationBean.setOrder(1);
return registrationBean;
}
}
Step 2: Create the Filter Class
package com.gigshack.api.middleware;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@Slf4j
public class HttpRequestLoggingFilter extends OncePerRequestFilter {
public static boolean logResponseEnabled = false;
public static List<String> uris = new ArrayList<>();
public static String allowedIpFilter = null; // Static variable to filter by IP address
private void logRequestAsCurl(HttpServletRequest request, ContentCachingResponseWrapper wrappedResponse) {
String curlCommand = buildCurlCommand(request);
log.info(curlCommand);
if (logResponseEnabled) {
logResponse(wrappedResponse);
}
}
private String buildCurlCommand(HttpServletRequest request) {
StringBuilder curl = new StringBuilder("\u001B[1;33m======= CURL COMMAND =======\n" + "\u001B[0m").append("curl -X ").append(request.getMethod());
List<String> ignoreHeaders = List.of("host", "authorization", "postman-token", "connection", "accept-encoding", "origin", "referer", "content-length", "user-agent", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "accept-language");
// Add headers to the curl command
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
if (!ignoreHeaders.contains(header.toLowerCase())) {
curl.append(" -H \"").append(header).append(": ").append(request.getHeader(header)).append("\"");
}
}
// Check if it's a multipart request
boolean isMultipart = request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/");
if (isMultipart) {
// Handle multipart form data
appendMultipartFormData(request, curl);
} else {
// Handle regular request body
String requestBody = getRequestPayload(request);
if (!requestBody.isBlank()) {
curl.append(" --data '").append(requestBody.replace("'", "\\'")).append("'");
}
}
// Add URL and query parameters
String requestURL = request.getRequestURL().toString();
if (request.getQueryString() != null) {
requestURL += "?" + request.getQueryString();
}
curl.append(" \"").append(requestURL).append("\"");
return curl.toString();
}
private void appendMultipartFormData(HttpServletRequest request, StringBuilder curl) {
// For multipart requests, add form fields to curl command
request.getParameterMap().forEach((key, values) -> {
if (values != null) {
for (String value : values) {
curl.append(" -F \"").append(key).append("=").append(value.replace("\"", "\\\"")).append("\"");
}
}
});
// Log file field names (without the actual file content)
try {
for (jakarta.servlet.http.Part part : request.getParts()) {
String name = part.getName();
String filename = part.getSubmittedFileName();
// If this part has a filename, it's a file upload
if (filename != null && !filename.isEmpty()) {
// Only add the field name for file uploads, not the actual file
curl.append(" -F \"").append(name).append("=@file_placeholder\"");
log.info("File field detected: {} (filename: {})", name, filename);
}
}
} catch (Exception e) {
log.warn("Could not process multipart parts: {}", e.getMessage());
}
}
private String getRequestPayload(HttpServletRequest request) {
// For multipart requests, we handle them separately in appendMultipartFormData
if (request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/")) {
// Still log parameters for debugging, but don't include in payload
logMultipartParameters(request);
return "";
}
byte[] buf = ((ContentCachingRequestWrapper) request).getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, 5120); // Limit to first 5KB
try {
return new String(buf, 0, length, request.getCharacterEncoding());
} catch (IOException ex) {
return "[unknown encoding]";
}
}
return "";
}
private void logMultipartParameters(HttpServletRequest request) {
// Log the regular form fields
request.getParameterMap().forEach((key, value) -> {
if (value != null && value.length > 0) {
log.info("Form field: {} = {}", key, String.join(", ", value));
}
});
// Also log file parts for better debugging
try {
for (jakarta.servlet.http.Part part : request.getParts()) {
String name = part.getName();
String filename = part.getSubmittedFileName();
// If this part has a filename, it's a file upload
if (filename != null && !filename.isEmpty()) {
log.info("File part: {} (filename: {}, size: {} bytes)",
name, filename, part.getSize());
}
}
} catch (Exception e) {
log.warn("Could not process multipart parts for logging: {}", e.getMessage());
}
}
@Override
public void destroy() {
// Cleanup logic (if needed)
}
@Override
protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// Wrap the original request in ContentCachingRequestWrapper to read the body safely
HttpServletRequest wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) httpServletRequest);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(httpServletResponse);
// Proceed with the filter chain (allow the request to reach its destination)
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
// Check the IP filter condition before logging
if (allowedIpFilter == null || allowedIpFilter.isBlank() || allowedIpFilter.equals(httpServletRequest.getRemoteAddr())) {
String uri = wrappedRequest.getRequestURI();
String clientIp = wrappedRequest.getRemoteAddr();
if (uris == null || uris.isEmpty()) {
log.info("\u001B[94mOUTGOING => [{}] {}\u001B[0m", clientIp, uri); // normal
logRequestAsCurl(wrappedRequest, wrappedResponse);
} else if (uris.stream().anyMatch(uri::startsWith)) {
log.info("\u001B[1;95mOUTGOING => [{}] {}\u001B[0m", clientIp, uri);
logRequestAsCurl(wrappedRequest, wrappedResponse);
}
// Very important: copy the content back to the actual response
try {
if (!httpServletResponse.isCommitted()) {
wrappedResponse.copyBodyToResponse();
}
} catch (ClientAbortException e) {
log.warn("Client aborted connection while writing response");
} catch (IOException e) {
log.warn("IOException during response copy: {}", e.getMessage());
}
}
}
}
private void logResponse(ContentCachingResponseWrapper response) {
byte[] buf = response.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, 51200); // Limit to 50KB
try {
String payload = new String(buf, 0, length, response.getCharacterEncoding());
log.info("Response({}): {}", response.getStatus(), payload);
} catch (IOException ex) {
log.warn("Unable to read response body");
}
} else {
log.info("Response({}): [empty]", response.getStatus());
}
}
}
Sample Output:
Request: /authenticate
curl -X POST -H "accept: */*" -H "content-type: application/json" -H "content-length: 48" --data '{ "username": "abc", "password": "helloworld"}' "http://localhost:9098/authenticate"
2025-04-11 11:53:11.418 INFO 27784 --- [nio-9098-exec-2] c.g.api.config.HttpRequestLoggingFilter :
Response(500): {"code":500,"status":"500 INTERNAL_SERVER_ERROR","message":"Access forbidden"}
HandlerInterceptorbut that may not work well with logging the response as mentioned in the answer: concretepage.com/spring/spring-mvc/… - HandlerInterceptor has access to the method (method: "UsersController.getUser") though. That's not known in a servlet filter.LogClass{ getRequestAndSaveIt()} Gson.toJson(LogClass)as pseudocode