Java开发中如何获取执行失败的HTTP请求的请求体
问题根因
你当前使用的Spring自带ContentCachingRequestWrapper采用被动缓存机制:只有下游逻辑(控制器、参数解析器、后续过滤器)主动调用getInputStream()或getReader()读取请求体后,内容才会被写入缓存数组。
如果请求失败发生在请求体被读取之前(比如请求头校验不通过、权限拦截器直接拒绝、参数解析前置校验失败),缓存数组未被赋值,读取自然返回空字符串。
修复方案
对Filter逻辑做两处修改即可:
- 调用过滤器链前主动读取一次请求体,提前触发缓存,保证无论后续是否成功处理,缓存中都有请求体内容
- 把日志收集逻辑放入
try-finally块,避免过滤器链抛出异常时跳过日志收集
修改后的完整代码如下:
@Component @Slf4j public class HttpLogFilter extends OncePerRequestFilter { // 跳过文件上传类请求,避免读取请求体导致文件上传失败 private static final List<String> SKIP_CONTENT_TYPE = Arrays.asList(MediaType.MULTIPART_FORM_DATA_VALUE); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { long startTime = System.currentTimeMillis(); String requestId = UUID.randomUUID().toString(); response.addHeader(REQUEST_ID_PROPERTY_NAME, requestId); ThreadContext.put(REQUEST_ID_PROPERTY_NAME, requestId); ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); HttpLogDTO httpLog; try { // 非文件上传请求提前读取一次请求体,触发缓存 String contentType = request.getContentType(); if (StringUtils.isBlank(contentType) || SKIP_CONTENT_TYPE.stream().noneMatch(contentType::startsWith)) { // Spring自带StreamUtils不会关闭流,不影响后续逻辑读取请求体 StreamUtils.copyToByteArray(requestWrapper.getInputStream()); } filterChain.doFilter(requestWrapper, responseWrapper); } finally { // 无论请求是否抛出异常,都执行日志收集 httpLog = collectHttpInfo(requestWrapper, responseWrapper); responseWrapper.copyBodyToResponse(); long endTime = System.currentTimeMillis(); httpLog.setDuration(endTime - startTime); log.info(httpLog.toString()); ThreadContext.remove(REQUEST_ID_PROPERTY_NAME); } } private HttpLogDTO collectHttpInfo(HttpServletRequest request, HttpServletResponse response) { String scheme = request.getScheme(); String host = request.getServerName(); String port = String.valueOf(request.getLocalPort()); String path = request.getServletPath(); String parameters = request.getParameterMap().keySet().stream() .map(key -> key + "=" + request.getParameterMap().get(key)[0]).collect(Collectors.joining("&")); Map<String, List<String>> requestHeaders = Collections.list(request.getHeaderNames()) .stream() .collect(Collectors.toMap( Function.identity(), h -> Collections.list(request.getHeaders(h)) )); Map<String, String> responseHeaders = response.getHeaderNames() .stream() .collect(Collectors.toMap(h -> h, response::getHeader)); // 读取内容时指定字符集,避免中文乱码 String charEncoding = StringUtils.defaultIfBlank(request.getCharacterEncoding(), StandardCharsets.UTF_8.name()); String requestBody = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), Charset.forName(charEncoding)); String responseBody = new String(((ContentCachingResponseWrapper) response).getContentAsByteArray(), Charset.forName(charEncoding)); return HttpLogDTO.builder() .protocol(request.getProtocol()) .remote(request.getRemoteAddr()) .method(request.getMethod()) .uri(scheme + "://" + host + ":" + port + path + (StringUtils.isNotBlank(parameters) ? "?" + parameters : "")) .host(host) .path(path) .scheme(scheme) .port(port) .requestHeaders(requestHeaders.toString()) .requestBody(requestBody) .statusCode(String.valueOf(response.getStatus())) .statusValue(Objects.requireNonNull(HttpStatus.resolve(response.getStatus())).getReasonPhrase()) .responseHeaders(responseHeaders.toString()) .responseBody(responseBody) .build(); } }
额外注意
如果你的服务有大请求体场景,可以设置缓存大小阈值,超过阈值的请求体不缓存,避免占用过多内存导致OOM。
内容的提问来源于stack exchange,提问作者Oguzhan Cevik
相关产品推荐
相关产品推荐

