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._
<p>def uploadXml = Action(parse.multipartFormData) { request =>
request.body.file("xmlFile") match {
case Some(filePart) =>
val factory = SAXParserFactory.newInstance()
factory.setFeature("<a href="https://www.php.cn/link/bdc31e3996f71d8f34782dda5ea48511">https://www.php.cn/link/bdc31e3996f71d8f34782dda5ea48511</a>", true)
factory.setFeature("<a href="https://www.php.cn/link/4d1025a728b16caa6ca38ed00c663e68">https://www.php.cn/link/4d1025a728b16caa6ca38ed00c663e68</a>", false)
factory.setFeature("<a href="https://www.php.cn/link/a6668ef0ca3092b1efab304fbf65e4e6">https://www.php.cn/link/a6668ef0ca3092b1efab304fbf65e4e6</a>", false)</p><pre class='brush:php;toolbar:false;'> val parser = factory.newSAXParser()
val handler = new DefaultHandler() // 替换为你自己的逻辑
parser.parse(filePart.ref().path.toFile, handler)
Ok("XML parsed safely")
case None => BadRequest("Missing file part 'xmlFile'")} }
关键点:
- 用
SAXParser替代scala.xml.XML,控制内存占用 - 显式禁用 DTD、外部实体(
disallow-doctype-decl等三项必须设为true) -
filePart.ref().path是 Play 自动保存的临时文件,无需自己流式复制 - 字段名
"xmlFile"必须和 HTML 表单中<input name="xmlFile" type="file">一致
如何在测试中模拟 XML 文件上传
用 Helpers.route() + MultipartFormData 构造体时,别直接塞字符串——Play 的测试工具链要求你提供 FilePart 的二进制数据和 MIME 类型:
import play.api.test._
import java.io.File
<p>val xmlBytes = "<root><item>test</item></root>".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
)</p><p>val request = FakeRequest(POST, "/upload").withMultipartFormDataBody(formData)
val result = route(app, request).get</p>注意:TemporaryFile 构造器第二个参数是 Array[Byte],不是 String;MIME 类型写 "application/xml" 比 "text/xml" 更稳妥,部分浏览器和 Play 内部逻辑会据此做不同处理。
临时文件路径和解析时机容易混淆:Play 在 action 执行前就已把上传文件落地为磁盘临时文件,filePart.ref().path 是真实可读的路径,但该文件会在 action 返回后被自动清理——所以不要在异步逻辑里延迟读取,否则可能报 No such file。


















