Scala Play框架如何处理XML文件上传 MultipartFormData

9次阅读

Play 2.8+ 中需手动提取 FilePart 并用禁用 DTD 的 SAXParser 安全解析 xml,避免 OOM 和 XXE;request.body.asMultipartFormData() 返回 NULL 多因 enctype、http 方法、路由或 Content-Type 错误。

Scala Play框架如何处理XML文件上传 MultipartFormData

Play 2.8+ 中如何接收 XML 文件上传(MultipartFormData

Play 默认不自动解析 multipart 请求中的 XML 文件体——它只把整个 part 当作原始字节流或字符串处理,不会像 jsON 那样触发隐式反序列化。你必须手动提取 FilePart,再读取其内容并解析为 XML。

request.body.asMultipartFormData() 返回 null 的常见原因

这个方法返回 null 通常不是因为没传文件,而是请求根本没走对路:

  • 前端没设 enctype="multipart/form-data"(比如用 application/json 发送)
  • HTTP 方法不是 POSTPUT
  • Play 路由没匹配到对应 action(比如用了 GET 路由去接上传)
  • Content-Type 头缺失或格式错误(如写成 multipart/form-data; 少了 boundary)

检查日志里是否有 WARN - Body parser failed: MultipartFormData body parsing failed,这说明解析器中途放弃了。

安全地读取并解析 XML 文件内容(避免 OOM 和 XXE)

直接调用 filePart.ref().path 拿到临时文件路径后,不能用 scala.xml.XML.load() —— 它默认启用 DTD 解析,存在 XXE 漏洞;也不能无限制读大文件进内存。

import javax.xml.parsers.SAXParserFactory import org.xml.sax.helpers.DefaultHandler import play.api.mvc._ 

def uploadXml = Action(parse.multipartFormData) { request => request.body.file("xmlFile") match { case Some(filePart) => val factory = SAXParserFactory.newinstance() factory.setFeature("https://www.php.cn/link/bdc31e3996f71d8f34782dda5ea48511", true) factory.setFeature("https://www.php.cn/link/4d1025a728b16caa6ca38ed00c663e68", false) factory.setFeature("https://www.php.cn/link/a6668ef0ca3092b1efab304fbf65e4e6", false)

  val parser = factory.newSAXParser()   val handler = new DefaultHandler() // 替换为你自己的逻辑   parser.parse(filePart.ref().path.toFile, handler)    Ok("XML parsed safely") case None =youjiankuohaophpcn BadRequest("Missing file part 'xmlFile'")

} }

关键点:

  • SAXParser 替代 scala.xml.XML,控制内存占用
  • 显式禁用 DTD、外部实体(disallow-doctype-decl 等三项必须设为 true
  • filePart.ref().path 是 Play 自动保存的临时文件,无需自己流式复制
  • 字段名 "xmlFile" 必须和 html 表单中 一致

如何在测试中模拟 XML 文件上传

Helpers.route() + MultipartFormData 构造体时,别直接塞字符串——Play 的测试工具链要求你提供 FilePart 的二进制数据和 MIME 类型:

import play.api.test._ import java.io.File 

val xmlBytes = "test".getBytes("UTF-8") val formData = MultipartFormData[TemporaryFile]( dataParts = Map.empty, files = Seq( FilePart("xmlFile", "data.xml", Some("application/xml"), TemporaryFile("test.xml", xmlBytes)) ), badParts = Seq.empty )

val request = FakeRequest(POST, "/upload").withMultipartFormDataBody(formData) val result = route(app, request).get

注意:TemporaryFile 构造器第二个参数是 Array[Byte],不是 String;MIME 类型写 "application/xml""text/xml" 更稳妥,部分浏览器和 Play 内部逻辑会据此做不同处理。

临时文件路径和解析时机容易混淆:Play 在 action 执行前就已把上传文件落地为磁盘临时文件,filePart.ref().path 是真实可读的路径,但该文件会在 action 返回后被自动清理——所以不要在异步逻辑里延迟读取,否则可能报 No such file

text=ZqhQzanResources