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_routeuri: http://localhost:8080/uploadpredicates:- Path=/uploadfilters:- name: RequestSizeargs: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";@Overridepublic 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()) {@Overridepublic Flux<DataBuffer> getBody() {return body;}};return chain.filter(exchange.mutate().request(decorator).build());}return chain.filter(exchange);}@Overridepublic 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 monoMono<?> modifiedBody = serverRequest.bodyToMono(inClass)// .log("modify_request_mono", Level.INFO).flatMap(body -> {// TODO: migrate to asyncboolean 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的查、寄的能力,短时间就搭建了功能齐全的快递业务中台,笔者觉得这种思路非常可取,后续也希望能学习一下,也快速构建自己中台。

multipart/form-data遇上IOException:Stream ended unexpectedly相关推荐

  1. IE浏览器上传文件报错:org.apache.tomcat.util.http.fileupload.FileUploadException: Stream ended unexpectedly

    报错内容: 13:44:28.122 [http-nio-8081-exec-13] ERROR c.d.f.w.e.GlobalExceptionHandler - [notFount,64] - ...

  2. Stream ended unexpectedly异常

    测试中发现存在部分文件上传时抛出异常. org.springframework.web.multipart.MultipartException: Failed to parse multipart ...

  3. Stream ended unexpectedly

    Stream ended unexpectedly 提示一下:我出现这个错误的情况.最近在做小程序,后端框架用的是springboot.出现这种情况纯属是因为我用了花生壳的缘故.如果你们不是,就别往下 ...

  4. 1. 恼人的Multipart form data

    文章目录 1. 概述 2. 问题 3. 解决方案 3.1 解决方案一 3.2 解决方案二 3.3 解决方案三 4. 总结 1. 概述 我目前在公司负责开放平台项目,使用spring-cloud-gat ...

  5. Processing of multipart/form-data request failed. Stream ended unexpectedly

    RestTemplate 上传文件时报错,原因是请求头中的content-length字段记录的请求报文大小跟实际的body中的请求报文大小不一致,处理方法是去掉请求头中的content-length ...

  6. IE报错 Stream ended unexpected

    记录遇到的一个IE的神奇报错: 环境: IE11 HTML代码: <form class="layui-form layui-form-pane" id="cryp ...

  7. flux读取不到数据_WebFlux 中form data获取不到参数问题

    Spring WebFlux 中, request.queryParams 只能获取到 查询参数, 对于 form 提交的参数无法进行参数自动装载 处理方式有两种: 一. 自定义 ArgumentRe ...

  8. SpringBoot 项目上传文件异常【java.io.IOException: Stream closed】

    项目场景: 提示:这里简述项目相关背景: 项目场景:SpringBoot 项目上传文件接口异常 21 十二月 2022 13:30:53,132 36991 [http-nio-9220-exec-3 ...

  9. [当人工智能遇上安全] 2.清华张超老师 - GreyOne: Discover Vulnerabilities with Data Flow Sensitive Fuzzing

    您或许知道,作者后续分享网络安全的文章会越来越少.但如果您想学习人工智能和安全结合的应用,您就有福利了,作者将重新打造一个<当人工智能遇上安全>系列博客,详细介绍人工智能与安全相关的论文. ...

最新文章

  1. Linux 运维常用命令 find、awk、sed、grep、vi、ps、lsof、rpm
  2. python3 操作redis
  3. OC HelloWord开始学习 1
  4. js循环(for/for in/forEach/map/for of)详解
  5. 实现权限控制_Spring自定义注解+AOP实现权限控制
  6. js 引用 java常量_java调用JS 与JS 调java
  7. Verify the Developer App certificate for your account is trusted on your device.
  8. linux转换vcf格式,如何使用awk分割vCard通讯录文件(.vcf)
  9. 前端学习(1663):前端系列实战课程之禁止保存
  10. @@IDENTITY与SCOPE_IDENTITY()
  11. jq 和java 多张图片_jQuery多个事件触发相同的功能
  12. 2018杭电多校第二场1006(容斥原理,组合数学)
  13. 最课程学员启示录:一份有诚意的检讨书
  14. 用gallery展示图片,实现中间图片稍大,两边较小的效果
  15. Android客户端实现session会话过期的功能
  16. 蓝牙模块配置串口通讯
  17. 层次分析法(AHP)原理以及应用
  18. JP1081B/9700_USB网卡驱动
  19. sqlitespy可以打开MySQL吗_sqlitespy下载
  20. ROS中执行roslaunch后,显示功能包不存在的解决方法

热门文章

  1. matlab 功率谱密度 汉宁窗_信号系统的一些基本概念
  2. 基于FPGA的DDS在Vivado中仿真以及在ZYNQ7020上板的实现(1)
  3. 海马手机助手怎么开启本地服务器,深度操作系统 V20(1003)内测版招募:新增手机助手,支持安卓和 iOS 端,管理手机应用、文件...
  4. 【ESP32 S2 烧录AT固件 串口与USB两种方式】
  5. 稳定同位素标记谱图可作为另一维数据
  6. 如何向人讨教--摘自《人性的弱点》
  7. java服务占用cpu居高不下问题排查
  8. strut2中使用ajax主题时出现 template/ajax/head.ftl. 错误的解决办法
  9. mysql回滚工具_MySQL回滚工具binlog2sql使用介绍
  10. 与小卡特一起学python 豆瓣_父与子的编程之旅:与小卡特一起学Python