multipartform-data遇上IOException:Stream ended unexpectedly

tech2025-07-08  1

IOException:Stream ended unexpectedly

前阵子在使用spring cloud gateway的过程中,遇到图片上传出现异常:Processing of multipart/form-data request failed. Stream ended unexpectedly(流异常关闭)。 从异常信息可以看到,springMvc在处理multipart/form-data格式的请求中,使用了StandardServletMultipartResolver对请求进行解析并做数据转换,在转换数据的过程中,遇到了异常,在没有正常解析到预期数据的情况下,流读取已经完毕,因此抛了此异常。因此可以分析,有可能是服务在接收请求的过程中,数据有丢失的情况。

数据丢失排查

1、一般网关会有文件上传大小限制,排查网关文件大小限制的问题; 2、排查请求经过spring cloud gateway数据被修改;

文件大小限制

spring cloud gateway 官方示例配置:

spring: cloud: gateway: routes: - id: request_size_route uri: http://localhost:8080/upload predicates: - Path=/upload filters: - name: RequestSize args: maxSize: 5000000

当请求体超过限制被拒,RequestSize GatewayFilter 将设置响应状态为413 Payload Too Large并携带报头errorMessage。以下示例显示了这样的内容errorMessage:

errorMessage` : `Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB

maxSize默认请求大小为:5 MB。 由于网关并没直接返回以上异常信息,所以排除是文件大小限制问题导致。

spring cloud gateway 修改了请求体?

2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Loggers.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] Increasing pending responses, now 1 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ COMPLETE 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Loggers.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] [HttpServer] Handler is being applied: org.springframework.http.server.reactive.ReactorHttpHandlerAdapter@7ffec058 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|DefaultPartBodyStreamStorageFactory.java|<init>|Temporary folder: /tmp/nio-file-upload 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|DefaultPartBodyStreamStorageFactory.java|<init>|Temporary folder: /tmp/nio-file-upload 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Loggers.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] Subscribing inbound receiver [pending: 1, cancelled:false, inboundDone: false] 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 16384B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 00 02 08 20 80 00 02 08 20 80 00 02 08 20 80 00 |... .... .... ..| |00000010| 02 08 20 80 00 02 08 20 80 00 02 08 20 80 00 02 |.. .... .... ...| |00000020| 61 16 20 81 33 cc e0 9c 0e 01 04 10 40 00 01 04 |a. .3.......@...| |00000030| 10 40 00 01 04 10 40 00 01 04 10 40 00 01 04 10 |.@....@....@....| |00003ff0| a0 60 d1 d4 c8 0f c6 7a 57 b1 57 fd 6e 70 f8 38 |.`.....zW.W.np.8| +--------+-------------------------------------------------+----------------+ 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 65536B 2020-09-04 16:26:19|[reactor-http-server-epoll-5]|Slf4JLogger.java|debug|[id: 0xd99e8e2b, L:/[网关] - R:/[客户端]] READ: 23669B

上面这段日志是客户端发起请求到netty接收后打印的debug日志,可以算出该包体的总字节数是:696,437B,与笔者上传的图片文件相差无几,其中除去请求体里其他的字符,应该跟图片大小一致。

2020-09-04 16:26:19|[reactor-http-client-epoll-13]|Slf4JLogger.java|debug|[id: 0x0cb9f380, L:/[网关] - R:[服务端]] FLUSH 2020-09-04 16:26:19|[reactor-http-client-epoll-13]|Loggers.java|debug|[id: 0x0cb9f380, L:/[网关] - R:[服务端]] Writing object 2020-09-04 16:26:19|[reactor-http-client-epoll-13]|Slf4JLogger.java|debug|[id: 0x0cb9f380, L:/[网关] - R:[服务端]] WRITE: 1212249B

而上面这段日志是netty转发业务层处理好的请求到【服务端】的日志,可以看到总包体字节总数为:1,212,249B,与客户提交的包体相差近1倍。因此可以断定,是因为请求体大小变化了,而请求头中Content-Length: 696161,并没有产生变化,从而导致【服务端】解析报文出错,进而出现IO异常,Stream ended unexpectedly。

笔者曾在这个基础上做过实验,在【网关】把头部的Content-Length移除,再设置请求头Transfer-Encoding: chunked,实现分块编码传输,最终【服务端】能正常接收【网关】的请求,没报IO异常,解析multipart/form-data也正常,只是保存的图片大小已经变成1.2M,与原始图片大小(679KB)相差很大,并且无法打开。

请求体经由gateway之后为什么发生变化

由上述debug日志可以看出,netty在接收后的请求是正常的,说明问题是在更上层的业务层发生的,因此笔者先断点到SCG(spring cloud gateway,后续使用此简称)的第一个全局filter

public class AdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered { public static final String CACHED_REQUEST_BODY_KEY = "cachedRequestBody"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Flux<DataBuffer> body = exchange.getAttributeOrDefault(CACHED_REQUEST_BODY_KEY, null); if (body != null) { ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return body; } }; return chain.filter(exchange.mutate().request(decorator).build()); } return chain.filter(exchange); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1000; } }

从断点中可以看到,此处从ServerWebExchange中获取"cachedRequestBody"(缓存的body),已经是被改变后的请求体了,所以问题还出现在更前面。顺着思路,我们查找设置"cachedRequestBody"的地方,还是依赖断点。

public class DefaultServerWebExchange implements ServerWebExchange { public Map<String, Object> getAttributes() { return this.attributes; } }

我们找到了ServerWebExchange的实现类:DefaultServerWebExchange,通过断点getAttributes找到设置body的入口处。最后笔者在ReadBodyPredicateFactory中找到设置入口:

public AsyncPredicate<ServerWebExchange> applyAsync(Config config) { return exchange -> { Class inClass = config.getInClass(); ServerRequest serverRequest = new DefaultServerRequest(exchange); // TODO: flux or mono Mono<?> modifiedBody = serverRequest.bodyToMono(inClass) // .log("modify_request_mono", Level.INFO) .flatMap(body -> { // TODO: migrate to async boolean test = config.predicate.test(body); exchange.getAttributes().put(TEST_ATTRIBUTE, test); return Mono.just(body); }); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, inClass); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getRequest().getHeaders()); return bodyInserter.insert(outputMessage, new BodyInserterContext()) // .log("modify_request", Level.INFO) .then(Mono.defer(() -> { boolean test = (Boolean) exchange.getAttributes() .getOrDefault(TEST_ATTRIBUTE, Boolean.FALSE); exchange.getAttributes().remove(TEST_ATTRIBUTE); exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, outputMessage.getBody()); return Mono.just(test); })); }; }

顺着断点往里走,最终在CharSequenceEncoder类中发现了问题的根源:

public Flux<DataBuffer> encode(Publisher<? extends CharSequence> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { Charset charset = this.getCharset(mimeType); return Flux.from(inputStream).map((charSequence) -> { CharBuffer charBuffer = CharBuffer.wrap(charSequence); ByteBuffer byteBuffer = charset.encode(charBuffer); return bufferFactory.wrap(byteBuffer); }); } static { DEFAULT_CHARSET = StandardCharsets.UTF_8; }

看到这里,就已经可以明白问题是怎么发生的了,由于在用户在上传文件的时候,经过SCG,使用了默认编码UTF-8对请求体进行解码后缓存到ServerWebExchange,所以导致图片格式的文件,最终字节数变大原因。在此之前,笔者也做了实验,用UTF-8编码的txt文件上传则不会出现问题,另外也采用了ISO-8859-1作为编码格式对图片进行上传,也不会出现问题,到这里就结案了。

解决方法: 客户端在上传图片的时候,指定编码ISO-8859-1,完美解决问题。

Content-Type: multipart/form-data;charset=iso-8859-1; boundary=-------------------------acebdf13572468 User-Agent: Fiddler Content-Length: 696149

结束语

笔者在遇到此问题的时候,第一时间还是到网上查找资料,但发现没有一篇文章跟我的情况一样,因此写此文章希望能帮到广大码友,特别是那些准备使用spring cloud gateway的朋友,提前可以了解到这个问题。

笔者近期也是听闻很多码友开始使用spring cloud构建微服务,之前有个朋友是做电商平台的,说他们正在搭建中台,而他们的做法,是在构建中台能力中引用外部的各种能力进行整合,从而快速实现自己的中台。 就比如他们做电商平台的,做了个快递中台,其中对接了快递100的查、寄的能力,短时间就搭建了功能齐全的快递业务中台,笔者觉得这种思路非常可取,后续也希望能学习一下,也快速构建自己中台。

最新回复(0)