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

Play 2.8+ 中如何接收 XML 文件上传(MultipartFormData)
Play 默认不自动解析 multipart 请求中的 XML 文件体——它只把整个 part 当作原始字节流或字符串处理,不会像 JSON 那样触发隐式反序列化。你必须手动提取 FilePart,再读取其内容并解析为 XML。
request.body.asMultipartFormData() 返回 null 的常见原因
这个方法返回 null 通常不是因为没传文件,而是请求根本没走对路:
-
前端没设
enctype="multipart/form-data"(比如用application/json发送) - HTTP 方法不是
POST或PUT - 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.Fileval xmlBytes = "
".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 ) - test
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。










