You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ContainerRequestFilter中读取multipart/form-data头且不加载完整文件

问题描述

我有一个用于将文件存入数据库的WebService,需要输出HTTP请求体来校验客户端实现是否正确。我尝试在Wildfly服务器上配置undertow过滤器,能获取部分信息但无法得到multipart/form-data头,因此我实现了一个实现ContainerRequestFilter接口的RequestLoggingFilter类,用于在使用resteasy facade时记录请求体。

我的facade标注了如下注解:

@POST
@Logged
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_PLAIN)

该实现可以正常运行,但当我使用ServletInputStream输出请求体时,会将整个文件加载到内存中。如果上传的是“大”文件,我的方法会抛出异常,且facade无法获取到可用的InputStream。

11:21:58,943 TRACE [project.ws.utils.RequestLoggingFilter] (default task-1) Error bodyio.undertow.server.RequestTooBigException: UT000020: Connection terminated as request was larger than 10485760
11:21:58,945 TRACE [project.ws.utils.RequestLoggingFilter] (default task-1) --------------------------------------------------------
11:21:58,954 WARN  [org.apache.james.mime4j.parser.MimeEntity] (default task-1) Unexpected end of headers detected. Higher level boundary detected or EOF reached.
11:21:58,956 WARN  [org.apache.james.mime4j.parser.MimeEntity] (default task-1) Invalid header encountered
11:21:58,960 WARN  [org.apache.james.mime4j.parser.MimeEntity] (default task-1) Body part ended prematurely. Boundary detected in header or EOF reached.
11:21:58,970 ERROR [project.ws.handler.exceptions.GlobalExceptionMapper] (default task-1) RESTEASY007500: Could find no Content-Disposition header within part: java.lang.RuntimeException: RESTEASY007500: Could find no Content-Disposition header within part

日志类代码如下:

@Logged
@Provider
public class RequestLoggingFilter implements ContainerRequestFilter{
    
    private static final Logger LOGGER = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        if (!LOGGER.isTraceEnabled()) {
            return;
        }
        LOGGER.trace("--------------------------------------------------------");
        LOGGER.trace("URI=" + requestContext.getUriInfo().getPath());
        LOGGER.trace("Content length=" + requestContext.getLength());
        LOGGER.trace("Content type=" + requestContext.getMediaType());
        LOGGER.trace("Methode=:" + requestContext.getMethod());
        LOGGER.trace("Cookies=:" + requestContext.getCookies());
        LOGGER.trace("MediaTypes=" + requestContext.getAcceptableMediaTypes());
        LOGGER.trace("AceptableLanguages=" + requestContext.getAcceptableLanguages());
        LOGGER.trace("Date=" + requestContext.getDate());
        LOGGER.trace("   Header:");
        requestContext.getHeaders().forEach((key, value) -> {
            LOGGER.trace("       header=" + key + " : " + value);
        });
        try {
            if (requestContext.hasEntity()) {
                String result = IOUtils.toString(requestContext.getEntityStream(), StandardCharsets.UTF_8);
                LOGGER.trace("   Body:");
                Scanner scanner = new Scanner(result);
                while (scanner.hasNextLine()) {
                    LOGGER.trace("        " + scanner.nextLine());
                }
                scanner.close();
                requestContext.setEntityStream(CharSource.wrap(result).asByteSource(StandardCharsets.UTF_8).openStream());
            }
        } catch (Exception e) {
            LOGGER.trace("Error body" + e);
        }
        LOGGER.trace("--------------------------------------------------------");
    }

}

运行日志如下:

--------------------------------------------------------
URI=/reception
Content length=259
Content type=multipart/form-data;boundary="----=_Part_43_341076338.1636107376952"
Methode=:POST
Cookies=:{}
MediaTypes=[*/*]
AceptableLanguages=[]
Date=null
    Header:
       header=Accept-Encoding : [gzip,deflate]
       header=apiKey : [test]
       header=Connection : [Keep-Alive]
       header=Content-Length : [259]
       header=Content-Type : [multipart/form-data; boundary="----=_Part_43_341076338.1636107376952"]
       header=Host : [localhost:9000]
       header=MIME-Version : [1.0]
       header=User-Agent : [Apache-HttpClient/4.1.1 (java 1.5)]
    Body:
        
         ------=_Part_43_341076338.1636107376952
         Content-Type: text/plain; charset=Cp1252; name=file3.tmp
         Content-Transfer-Encoding: binary
         Content-Disposition: form-data; name="file"; filename="file3.tmp"         
        ------=_Part_43_341076338.1636107376952--
 --------------------------------------------------------

请问在Java 8环境下,如何在ContainerRequestFilter中使用ServletInputStream时不加载整个文件,仅记录multipart/form-data头,同时保留InputStream处于打开状态供facade使用?


解决方案
  • 核心思路:不一次性读取整个请求流,基于Wildfly内置的mime4j流式解析能力仅读取multipart的头部分,遇到文件内容块就终止解析,同时用支持标记重置的输入流包装类恢复流初始位置,供后续facade正常读取。
  • 额外限制:设置最大预读取阈值,避免恶意超大请求占满内存。

改造后的核心代码

@Logged
@Provider
public class RequestLoggingFilter implements ContainerRequestFilter{
    private static final Logger LOGGER = LoggerFactory.getLogger(RequestLoggingFilter.class);
    // 最大预读取大小,可按需调整,1MB足够容纳所有multipart头信息
    private static final int MAX_READ_SIZE = 1024 * 1024;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        if (!LOGGER.isTraceEnabled()) {
            return;
        }
        LOGGER.trace("--------------------------------------------------------");
        LOGGER.trace("URI=" + requestContext.getUriInfo().getPath());
        LOGGER.trace("Content length=" + requestContext.getLength());
        LOGGER.trace("Content type=" + requestContext.getMediaType());
        LOGGER.trace("Methode=:" + requestContext.getMethod());
        LOGGER.trace("Cookies=:" + requestContext.getCookies());
        LOGGER.trace("MediaTypes=" + requestContext.getAcceptableMediaTypes());
        LOGGER.trace("AceptableLanguages=" + requestContext.getAcceptableLanguages());
        LOGGER.trace("Date=" + requestContext.getDate());
        LOGGER.trace("   Header:");
        requestContext.getHeaders().forEach((key, value) -> {
            LOGGER.trace("       header=" + key + " : " + value);
        });
        try {
            if (requestContext.hasEntity()) {
                MediaType mediaType = requestContext.getMediaType();
                // 仅处理multipart/form-data类型请求
                if (MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(mediaType)) {
                    String boundary = mediaType.getParameters().get("boundary");
                    if (boundary == null) {
                        return;
                    }
                    InputStream originalStream = requestContext.getEntityStream();
                    // 包装为支持mark/reset的流
                    BufferedInputStream bufferedStream = new BufferedInputStream(originalStream);
                    bufferedStream.mark(MAX_READ_SIZE);
                    
                    LOGGER.trace("   Multipart part headers:");
                    MimeConfig config = new MimeConfig.Builder().build();
                    MimeStreamParser parser = new MimeStreamParser(config);
                    parser.setContentHandler(new AbstractContentHandler() {
                        private boolean inPart = false;
                        @Override
                        public void startPart(BodyDescriptor bd) throws MimeException {
                            inPart = true;
                            LOGGER.trace("        --- Start part ---");
                        }
                        @Override
                        public void field(Field field) throws MimeException {
                            if (inPart) {
                                LOGGER.trace("        {}: {}", field.getName(), field.getBody());
                            }
                        }
                        @Override
                        public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
                            // 遇到body直接终止解析,不需要读取内容
                            throw new MimeException("Skip body content");
                        }
                    });
                    try {
                        parser.parse(bufferedStream);
                    } catch (MimeException e) {
                        // 预期的终止异常,无需处理
                    }
                    // 重置流位置,写回上下文供后续facade读取
                    bufferedStream.reset();
                    requestContext.setEntityStream(bufferedStream);
                } else {
                    // 非multipart请求保留原逻辑,增加大小判断
                    if (requestContext.getLength() < MAX_READ_SIZE) {
                        String result = IOUtils.toString(requestContext.getEntityStream(), StandardCharsets.UTF_8);
                        LOGGER.trace("   Body:");
                        new Scanner(result).forEachRemaining(line -> LOGGER.trace("        " + line));
                        requestContext.setEntityStream(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8)));
                    } else {
                        LOGGER.trace("   Body too large, skip logging");
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.trace("Error logging request: {}", e.getMessage());
        }
        LOGGER.trace("--------------------------------------------------------");
    }
}

注意事项

  1. 无需额外引入依赖,Wildfly和Resteasy已经内置mime4j、IOUtils相关类
  2. 不需要修改Undertow默认的最大请求大小限制,避免超大请求攻击
  3. 预读取的MAX_READ_SIZE建议设置为1MB以内,足够容纳常规multipart场景下所有头信息

内容的提问来源于stack exchange,提问作者Clément Gibert

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 02:18:01