Play Framework 2.5(Scala)请求流无法转为分块响应输出问题
问题分析与解决方案
你的问题核心在于请求体流与响应流的生命周期冲突:直接返回请求体的原始Source时,它和请求的输入流强绑定,当请求体接收完成后,底层输入流会被Play的HTTP服务器关闭,导致响应流无法正确完成数据传输,最终引发curl的transfer closed with outstanding read data remaining错误。而自定义Source能正常工作,是因为它完全独立于请求生命周期,流的创建、发送和终止都在可控范围内。
下面是具体的解决方案和解释:
一、使用Play内置的流式BodyParser(推荐)
Play提供了parse.verbatim这个专门为流式处理设计的BodyParser,它会正确管理请求体流的生命周期,避免直接使用Accumulator.source带来的冲突。
实现代码
import play.api.mvc._ import akka.util.ByteString def endpointA = Action(parse.verbatim) { request => // 保持响应的Content-Type与请求一致,避免类型不匹配问题 val contentType = request.headers.get("Content-Type").getOrElse("text/plain") Ok.chunked(request.body).as(contentType) }
如果必须使用EssentialAction:
def endpointA = EssentialAction { requestHeader => val parser = BodyParsers.parse.verbatim parser(requestHeader).map { parsedBody => val contentType = requestHeader.headers.get("Content-Type").getOrElse("text/plain") Ok.chunked(parsedBody).as(contentType) } }
二、解决Keep-Alive连接的兼容性问题
你设置了Connection: Keep-Alive,但Play在流式响应完成后,可能没有正确维持长连接的状态,导致curl误以为还有未传输的数据。可以尝试两种方式:
- 临时关闭Keep-Alive:测试时去掉
Connection和Keep-Alive头,看是否正常:
curl -v --data 'hello' -H "Content-type: text/plain" http://localhost:9584/binding-tariff-admin/upload-csv
- 强制响应关闭连接:如果必须保留Keep-Alive,但仍报错,可以在响应中显式添加
Connection: close头:
Ok.chunked(request.body).as(contentType).withHeaders("Connection" -> "close")
三、处理流的背压与数据传递
如果以上方案仍有问题,可以在请求体流上添加简单的转换,确保数据被正确传递到响应流(比如复制ByteString,避免原始流的引用问题):
def endpointA = Action(parse.verbatim) { request => val safeSource = request.body.map(bs => ByteString(bs.toArray)) val contentType = request.headers.get("Content-Type").getOrElse("text/plain") Ok.chunked(safeSource).as(contentType) }
四、检查Play版本
确保你使用的是Play 2.5.x的最新版本(比如2.5.18),早期的2.5版本存在一些流式处理的bug,升级后可能直接解决问题。
内容的提问来源于stack exchange,提问作者nfvindaloo
相关产品推荐
相关产品推荐

