You need to enable JavaScript to run this app. Java开发中如何获取执行失败的HTTP请求的请求体
优惠活动
大模型
产品
解决方案
定价
更多

Java开发中如何获取执行失败的HTTP请求的请求体

问题根因

你当前使用的Spring自带ContentCachingRequestWrapper采用被动缓存机制:只有下游逻辑(控制器、参数解析器、后续过滤器)主动调用getInputStream()或getReader()读取请求体后,内容才会被写入缓存数组。
如果请求失败发生在请求体被读取之前(比如请求头校验不通过、权限拦截器直接拒绝、参数解析前置校验失败),缓存数组未被赋值,读取自然返回空字符串。

修复方案

对Filter逻辑做两处修改即可:

  1. 调用过滤器链前主动读取一次请求体,提前触发缓存,保证无论后续是否成功处理,缓存中都有请求体内容
  2. 把日志收集逻辑放入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.08 15:45:00