用Fetch API发送XML数据时,请求体的构造方式会直接影响服务端能否正确解析。如果把一段XML字符串直接塞进body,却忘记设置Content-Type,服务端很可能按照普通文本处理,甚至直接返回415。反过来,如果使用FormData却手动指定了multipart/form-data,同样会因为缺少boundary而失败。这篇文章围绕Fetch POST XML的几种典型写法展开,把请求头、编码、后端接收和跨域排查一次性说清楚。

一、直接POST XML字符串:Content-Type与编码细节
当接口要求把XML作为原始请求体发送时,最简单的方式是把XML字符串交给body。比如订单系统需要向支付网关推送一段XML报文,可以这样写:
const xmlString = '<?xml version="1.0" encoding="UTF-8"?><user><name>张三</name></user>';
fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/xml; charset=UTF-8'
},
body: xmlString
}).then(function (response) {
return response.text();
}).then(function (data) {
console.log(data);
});
这里必须显式设置Content-Type。如果省略这个请求头,浏览器通常会按text/plain;charset=UTF-8处理字符串body,服务端若只配置了application/xml或text/xml解析器,就不知道请求体是XML。一般来说,application/xml比text/xml更通用,后者在一些老旧的WebService接口里仍然常见。对于新开发的服务,建议统一使用application/xml并加上charset=UTF-8。
中文内容尤其需要注意编码。JavaScript字符串在发送时默认按UTF-8编码,只要后端接收也按UTF-8解码,通常不会乱码。但如果XML声明里写了encoding等于GBK,而请求头却声明UTF-8,服务端就会在解析时出错。正确的做法是让XML声明、请求头中的charset和实际字节编码三者保持一致。如果XML来自用户上传的文件,还要先确认文件本身是否真的以UTF-8保存。
二、上传本地XML文件:File对象与Blob的差别
浏览器端的XML上传更多时候来自用户选择的本地文件。File对象可以直接作为Fetch的body,因为File继承了Blob。通过文件选择器拿到文件后,可以这样发送:
var fileInput = document.querySelector('#xmlFile');
fileInput.addEventListener('change', async function () {
var file = fileInput.files[0];
var response = await fetch('/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/xml'
},
body: file
});
var result = await response.text();
console.log(result);
});
这段代码里,querySelector获取的是页面上的<input type="file">元素。用户选择文件后,fileInput.files[0]就是一个File对象。可以把它直接放进body,并手动设置Content-Type为application/xml。如果不设置,浏览器会根据File对象的type属性决定请求头,但这个属性可能为空,或者来自操作系统而非真实内容,容易导致后端判断错误。
如果XML内容并不是来自文件,而是前端动态生成的一段字符串,用Blob可以更精确地控制类型。Blob构造函数的第二个参数能指定type,而且浏览器会把这个type作为请求体的内容类型,省去手动设置headers的麻烦。不过如果已经设置了Blob的type,就不需要再在headers里重复指定Content-Type,否则容易产生不一致。下面这种写法适合生成配置报文、拼接XML片段后上传:
var xml = '<?xml version="1.0" encoding="UTF-8"?><config><retry>3</retry></config>';
var blob = new Blob([xml], { type: 'application/xml; charset=UTF-8' });
fetch('/upload', {
method: 'POST',
body: blob
}).then(function (res) {
return res.text();
}).then(function (text) {
console.log(text);
});
File和Blob在使用上的核心区别在于数据来源:File来自用户磁盘,Blob通常在内存中生成。两者都能被Fetch接受,也都能保持二进制原样传输。对于XML这种文本格式,直接使用File或Blob通常比读出字符串再发送更省内存,也不用担心字符串拼接过程中破坏XML结构。
三、通过FormData上传XML文件:传统文件上传接口
不少后端接口把XML文件当成普通文件上传来接收,例如Spring Boot的MultipartFile。这时如果还想用直接POST字符串的方式,服务端会因为请求体不是multipart格式而报错。前端需要使用FormData,把文件放到表单字段中,交给后端解析。
var formData = new FormData();
formData.append('file', file);
formData.append('bizType', 'order');
fetch('/upload/multipart', {
method: 'POST',
body: formData
}).then(function (res) {
return res.json();
}).then(function (json) {
console.log(json);
});
使用FormData时有一个特别容易踩的坑:不要手动设置Content-Type。浏览器在发送FormData时会自动生成带boundary参数的multipart请求头,例如multipart/form-data; boundary=----WebKitFormBoundary...。如果手动写成multipart/form-data却没有boundary,服务端无法分隔不同字段,通常会返回400或500。
直接POST原始XML和FormData上传XML各有适用场景。下面这张对比表可以帮助判断:
| 方案 | Content-Type | 后端读取方式 | 适用场景 |
|---|---|---|---|
| 字符串/Blob/File | application/xml或text/xml | 读取请求体原始字符串 | WebService、XML API、内部服务 |
| FormData | multipart/form-data自动生成 | 读取文件参数 | 传统文件上传、带业务字段的表单 |
如果你的后端接口已经有MultipartFile这样的文件参数,就选FormData;如果接口直接暴露XML报文,就选第一种。两者不要混用,否则Content-Type与后端预期不匹配,排查起来会非常耗费时间。
四、后端接收示例:Node.js与Spring Boot
为了更快定位问题,需要知道后端如何接收。以Node.js的Express为例,如果前端直接POST XML字符串,可以使用express.text中间件,并指定它处理application/xml和text/xml两种类型:
const express = require('express');
const app = express();
app.use(express.text({ type: ['text/xml', 'application/xml'] }));
app.post('/upload', function (req, res) {
console.log(req.body);
res.send('ok');
});
app.listen(3000);
express.text会把请求体解析成字符串,并放在req.body中。这样前端直接发送的XML原始内容就能在服务端打印和处理。如果使用的是更轻量的原生Node服务,则可能需要自己收集data事件并拼接Buffer,例如用raw-body包来读取。
Spring Boot方面,接收原始XML通常用@RequestBody。在控制器方法上声明consumes为application/xml,这样Spring会使用XML消息转换器处理请求体,并将结果字符串注入方法参数:
@PostMapping(value = "/upload", consumes = {MediaType.APPLICATION_XML_VALUE})
public String upload(@RequestBody String xml) {
System.out.println(xml);
return "ok";
}
如果Spring Boot接口参数是@RequestParam的MultipartFile,前端就应该配合FormData方式。后端的consumes需要设置为multipart/form-data,或者干脆不加consumes,让框架根据请求自动适配。保持前后端对Content-Type的理解一致,是XML上传成功的关键。
五、跨域、预检与错误排查
当XML上传接口和前端页面不在同一个域名下时,CORS会成为一个额外关卡。直接POSTapplication/xml并不是简单请求,浏览器会先发送一个OPTIONS预检请求。服务端必须在预检响应中返回Access-Control-Allow-Headers: Content-Type,允许浏览器在正式请求中携带自定义或非简单的内容类型请求头。如果预检失败,控制台会显示CORS错误,而正式POST根本不会发出。
如果XML上传还需要携带用户登录态,Fetch默认不会发送Cookie。需要在init对象中设置credentials: include,同时服务端响应头不能是Access-Control-Allow-Origin: *,而必须指定具体域名,并设置Access-Control-Allow-Credentials: true。这些配置缺一不可,否则浏览器会拦截响应或拒绝保存Cookie。
在考虑跨域之前,更常见的错误还是状态码层面的。415几乎确认了Content-Type不符合后端预期;400通常说明XML格式错误或编码不一致;413表示请求体过大。打开浏览器开发者工具的Network面板,找到请求的Headers和Payload,先看Content-Type是不是application/xml,再看请求体是否完整。很多XML上传失败,最后都只是请求头少写了分号后的charset,或者多写了一个Content-Type导致boundary丢失。
因此,在使用Fetch上传XML时,先明确后端接口是原始报文还是文件字段,再选择字符串、File或者FormData。设置好Content-Type,保持编码一致,跨域配置到位,XML上传就能稳定工作。