如何在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("--------------------------------------------------------"); } }
注意事项
- 无需额外引入依赖,Wildfly和Resteasy已经内置mime4j、IOUtils相关类
- 不需要修改Undertow默认的最大请求大小限制,避免超大请求攻击
- 预读取的
MAX_READ_SIZE建议设置为1MB以内,足够容纳常规multipart场景下所有头信息
内容的提问来源于stack exchange,提问作者Clément Gibert
相关产品推荐
相关产品推荐

