如何修复multipart请求头与请求体中含无效字符的boundary分隔符
问题核心原因
你遇到的boundary带=的问题,本质是不符合RFC 2046对boundary参数未引号值的字符限制,你当前给boundary加双引号的处理其实已经符合HTTP规范,绝大多数multipart解析器(Tomcat内置、Apache Commons FileUpload、Spring MVC标准解析器)都可以正常识别,不需要修改请求体,可以先直接测试现有逻辑是否可用,大概率能满足需求。
现有代码bug修复
你当前的FixBoundaryRequestWrapper存在header返回重复的问题,会导致请求同时存在两个Content-Type头,需要修改两个方法:
@Override public Enumeration<String> getHeaderNames() { List<String> names = Collections.list(super.getHeaderNames()); // 先移除已被自定义覆盖的header,避免重复 names.removeAll(headerMap.keySet()); names.addAll(headerMap.keySet()); return Collections.enumeration(names); } @Override public Enumeration<String> getHeaders(String name) { // 自定义覆盖的header直接返回自定义值,不再拼接原始值 if (headerMap.containsKey(name)) { return Collections.enumeration(Collections.singletonList(headerMap.get(name))); } return super.getHeaders(name); }
如果确实需要修改请求体的实现方案
如果你使用的解析器确实不支持带引号的boundary,需要替换请求体中的boundary值,可以通过重写请求包装类的getInputStream()方法实现流级别的实时替换,不需要全量加载请求体,具体思路如下:
核心实现逻辑
- 在Filter中提取原始boundary,生成无特殊字符的新boundary,同时更新Content-Type头为使用新boundary的值
- 自定义ServletInputStream包装原始请求流,使用固定大小的滑动窗口匹配原始boundary分隔符,匹配到则输出新boundary,否则正常输出字节,内存占用仅和boundary长度相关
具体代码实现
1. 修改Filter逻辑,传递新旧boundary给包装类
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String contentTypeHeaderValue = request.getHeader(HttpHeaders.CONTENT_TYPE); // 非multipart请求直接放行 if (StringUtils.isBlank(contentTypeHeaderValue) || !contentTypeHeaderValue.startsWith("multipart/form-data")) { chain.doFilter(request, response); return; } Matcher matcher = headerValuePattern.matcher(contentTypeHeaderValue); if (!matcher.matches()) { chain.doFilter(request, response); return; } String oldBoundary = matcher.group(1); // 生成无特殊字符的新boundary,可按需调整生成规则 String newBoundary = oldBoundary.endsWith("=") ? oldBoundary.substring(0, oldBoundary.length() - 1) : oldBoundary; String updatedHeader = String.format("multipart/form-data; boundary=%s", newBoundary); FixBoundaryRequestWrapper wrappedRequest = new FixBoundaryRequestWrapper(request, oldBoundary, newBoundary); wrappedRequest.addHeader(HttpHeaders.CONTENT_TYPE, updatedHeader); chain.doFilter(wrappedRequest, response); }
2. 修改RequestWrapper,重写getInputStream方法
public class FixBoundaryRequestWrapper extends HttpServletRequestWrapper { private final Map<String, String> headerMap = new HashMap<>(); private final String oldBoundaryFull; private final String newBoundaryFull; private ServletInputStream wrappedInputStream; public FixBoundaryRequestWrapper(HttpServletRequest request, String oldBoundary, String newBoundary) { super(request); // multipart分隔符固定带--前缀 this.oldBoundaryFull = "--" + oldBoundary; this.newBoundaryFull = "--" + newBoundary; } // 省略原有addHeader、removeHeader、getHeader、getHeaderNames、getHeaders方法(使用上文修复后的版本) @Override public ServletInputStream getInputStream() throws IOException { if (wrappedInputStream == null) { wrappedInputStream = new BoundaryReplaceInputStream( super.getInputStream(), oldBoundaryFull.getBytes(StandardCharsets.ISO_8859_1), newBoundaryFull.getBytes(StandardCharsets.ISO_8859_1) ); } return wrappedInputStream; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding())); } }
3. 实现滑动窗口替换的自定义ServletInputStream
public class BoundaryReplaceInputStream extends ServletInputStream { private final ServletInputStream originalStream; private final byte[] matchBytes; private final byte[] replaceBytes; // 滑动匹配缓冲区,大小等于原始boundary完整长度 private final byte[] matchBuffer; private int bufferValidLength; // 替换内容溢出缓冲区,避免单次read输出多字节 private byte[] overflowBuffer = new byte[0]; private int overflowPos = 0; public BoundaryReplaceInputStream(ServletInputStream originalStream, byte[] matchBytes, byte[] replaceBytes) { this.originalStream = originalStream; this.matchBytes = matchBytes; this.replaceBytes = replaceBytes; this.matchBuffer = new byte[matchBytes.length]; // 初始化填充匹配缓冲区 this.bufferValidLength = originalStream.readNBytes(matchBuffer, 0, matchBuffer.length); } @Override public int read() throws IOException { // 优先读取溢出缓冲区的替换内容 if (overflowPos < overflowBuffer.length) { return overflowBuffer[overflowPos++] & 0xFF; } overflowPos = 0; // 缓冲区无有效数据,流已读完 if (bufferValidLength < matchBuffer.length) { return bufferValidLength == 0 ? -1 : matchBuffer[overflowPos++] & 0xFF; } // 检查是否匹配到原始boundary boolean isMatch = true; for (int i = 0; i < matchBytes.length; i++) { if (matchBuffer[i] != matchBytes[i]) { isMatch = false; break; } } if (isMatch) { overflowBuffer = replaceBytes; // 重新填充匹配缓冲区 bufferValidLength = originalStream.readNBytes(matchBuffer, 0, matchBuffer.length); return overflowBuffer[overflowPos++] & 0xFF; } // 不匹配则输出第一个字节,滑动缓冲区补读新字节 byte firstByte = matchBuffer[0]; System.arraycopy(matchBuffer, 1, matchBuffer, 0, matchBuffer.length - 1); int readByte = originalStream.read(); if (readByte == -1) { bufferValidLength--; } else { matchBuffer[matchBuffer.length - 1] = (byte) readByte; } return firstByte & 0xFF; } @Override public boolean isFinished() { return originalStream.isFinished() && bufferValidLength == 0 && overflowPos >= overflowBuffer.length; } @Override public boolean isReady() { return originalStream.isReady(); } @Override public void setReadListener(ReadListener readListener) { originalStream.setReadListener(readListener); } }
注意事项
- 该实现不会全量加载请求体,内存占用仅为boundary长度的2倍左右,适合大文件上传场景
- 如果新旧boundary长度不一致,不需要修改Content-Length头,Servlet容器会自动处理为chunked传输,不影响后续解析
- multipart结束分隔符
--${boundary}--会被自动正确替换,不需要额外处理
内容的提问来源于stack exchange,提问作者jimonthebarn
相关产品推荐
相关产品推荐

