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

如何修复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()方法实现流级别的实时替换,不需要全量加载请求体,具体思路如下:

核心实现逻辑

  1. 在Filter中提取原始boundary,生成无特殊字符的新boundary,同时更新Content-Type头为使用新boundary的值
  2. 自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 03:54:00