Spring Cloud Gateway调用getMultipartData读取multipart请求二次执行报错
问题原因
抛出java.lang.IllegalStateException: Only one connection receive subscriber allowed的核心原因是响应式WebFlux的流订阅规则和Spring Cloud Gateway的请求处理机制冲突,和单次请求内重复读取请求体无关:
- 直接调用
exchange.getMultipartData()解析multipart请求时,会直接订阅Netty底层的请求输入流读取数据,但解析完成后没有将结果缓存到ServerWebExchange的上下文属性中 - 首次请求执行时,原始请求流还未被任何逻辑订阅,所以解析逻辑可以正常运行;但请求校验通过后,后续的路由转发逻辑、其他内置过滤器会再次调用
getMultipartData()尝试读取请求体,此时会触发第二次对原始输入流的订阅,而Netty的入站接收流仅允许存在一个订阅者,直接抛出异常 - 原代码还存在额外风险:直接强转
FormFieldPart时如果对应表单字段不存在,会触发空指针异常。
正确实现方案
核心修复逻辑是:解析完multipart数据后,将结果缓存到ServerWebExchange的内置缓存属性中,后续所有读取multipart数据的逻辑会直接命中缓存,不会再重复订阅原始请求流。
可直接运行的实现代码如下:
@Slf4j @Component public class TestFilter implements GlobalFilter, Ordered { private final String validLoginId = "123456789"; private final String validToken = "987654321"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 非multipart请求直接放行,减少不必要的解析 MediaType contentType = exchange.getRequest().getHeaders().getContentType(); if (contentType == null || !MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) { return chain.filter(exchange); } return exchange.getMultipartData().flatMap(multipartData -> { // 安全读取表单字段,避免空指针 Part loginIdPart = multipartData.getFirst("loginId"); Part tokenPart = multipartData.getFirst("token"); if (!(loginIdPart instanceof FormFieldPart) || !(tokenPart instanceof FormFieldPart)) { return Mono.error(new IllegalArgumentException("Missing required auth parameters")); } String loginIdVal = ((FormFieldPart) loginIdPart).value(); String tokenVal = ((FormFieldPart) tokenPart).value(); log.info("Received loginId: {}, token: {}", loginIdVal, tokenVal); if (!validLoginId.equals(loginIdVal) || !validToken.equals(tokenVal)) { return Mono.error(new IllegalArgumentException("Auth parameter mismatch")); } // 关键:将解析完成的multipart数据写入网关内置缓存属性,后续逻辑不会再重复读取原始流 ServerWebExchange cachedExchange = ServerWebExchangeUtils.cacheMultipartData(exchange, multipartData); return chain.filter(cachedExchange); }); } @Override public int getOrder() { // 过滤器优先级设置为最高,确保在路由转发、其他请求解析逻辑之前执行 return Ordered.HIGHEST_PRECEDENCE; } }
额外注意事项
- 必须给过滤器设置执行顺序,确保在所有会读取请求体的逻辑之前运行,避免其他逻辑提前订阅原始流
- 如果需要修改multipart请求的内容(比如新增、修改表单字段),需要自己实现
ServerHttpRequestDecorator重写getBody()方法,重新生成multipart格式的请求体传递给下游服务 - 处理
DataBuffer时要注意引用计数,不需要使用的缓冲区要及时调用DataBufferUtils.release()释放,避免内存泄漏
内容的提问来源于stack exchange,提问作者Bernie.T
相关产品推荐
相关产品推荐

