尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

uni-app H5文件上传400错误排查:multipart boundary与Content-Type陷阱

uni-app H5文件上传400错误排查:multipart boundary与Content-Type陷阱 做uni-app的H5端开发文件上传算是我见过报错频率最高的一类功能。明明前端请求发出去了Network面板里看着也有东西后端就是收不到文件直接甩回来一个400文件上传失败。更气人的是同一套代码放App端跑得好好的一到H5就翻车。这篇文章我就把这个问题掰开揉碎讲清楚400到底是谁返回的、H5端上传和App端上传在底层有什么不一样、最常见的几个坑分别长什么样以及一套我实测过很多次、可以直接照抄的前后端写法。1. 先从现象入手400到底是谁返回的1.1 前置认知一次上传请求要穿过几道门很多人看到400就慌了觉得是后端接口写错了其实400这个状态码的含义是Bad Request也就是服务器觉得请求本身不合法根本没有走到业务代码里去。出了400第一步不是改代码而是先搞清楚这个400到底是哪一层返回的。一次H5上传请求从前端到后端业务方法至少要经过这么几道门浏览器这一侧uni.uploadFile在H5端底层用的是XMLHttpRequest浏览器负责把文件组装成multipart/form-data格式发出去。网关/Web服务器这一侧如果你部署了Nginx或其他网关请求会先到它这里它可能做大小限制、转发。后端框架这一侧Spring Boot这类框架会先解析请求体判断你是不是一个合法的multipart请求如果不是直接400根本到不了你的Controller。后端业务方法这一侧框架解析出MultipartFile之后再和你的方法参数做匹配名字对不上同样400。所以排查400核心思路就是逐层确认请求体到底是不是合法的multipart文件有没有真的被拼进去参数名对不对哪一层默认限制被触发了1.2 H5端uploadFile和App端、小程序端的底层差异这一步很多人一开始就漏了。uni-app的口号是一套代码多端运行但uploadFile在各个端上的底层实现差异巨大尤其是H5端和App端、小程序端完全是两套逻辑。运行端底层实现文件处理方式常见坑App端原生HTTP请求本地临时文件路径路径写错会直接本地报错一般到不了后端小程序端wx.uploadFile本地临时文件标识header设置限制多部分header会被过滤H5端XMLHttpRequest FormDatablob:临时URL或浏览器File对象Content-Type、blob路径、跨域都容易翻车在App端和小程序端你选择图片或文件之后拿到的是一个本地临时文件路径uploadFile接口拿到这个路径把文件二进制数据读出来拼到请求体里。整个过程是读文件拼请求。在H5端浏览器出于安全考虑根本不会给你一个可以随便读的真实磁盘路径它给你的是blob:http://localhost:8080/uuid这种临时对象URL或者直接给你一个浏览器File对象。uni.uploadFile在H5端的实现本质上就是把文件塞进一个FormData对象然后用XHR发出去。这个差异直接导致了很多在App端不会出现的问题后面几章我会逐个展开。2. 头号嫌疑手动写死Content-Type弄丢了multipart boundary2.1 boundary是后端拆包裹的封条要理解这个问题得先知道multipart/form-data是个什么东西。你可以把一次文件上传想象成寄快递请求体就是包裹里面可能放了文件、也可能放了几个表单字段每个部分之间用什么分隔靠的就是boundary。浏览器在创建FormData并发送时会自动生成一串随机的boundary值然后在Content-Type里带上它格式长这样Content-Type: multipart/form-data; boundary----WebKitFormBoundary7MA4YWxkTrZu0gW请求体里每一段数据前面也用这个boundary做分隔标记。后端的multipart解析器拿到请求之后第一件事就是去Content-Type里找boundary找到之后才能知道从哪儿到哪儿是一个字段从哪儿到哪儿是文件。这就像快递员拿到包裹得先看封条才知道里面分了几格。现在问题来了如果你在uni.uploadFile的header里手动写了Content-Type: multipart/form-data就相当于告诉浏览器你不用管Content-Type了我写好了。浏览器很听话按你的header发送但你这行代码里并没有boundary于是后端收到了一个声称自己是multipart、却又没有boundary的请求解析器当场傻眼直接判定请求不合法返回400。2.2 同样的代码为什么App端没事、H5端就400这是个非常常见的现象很多人都会问我header里写了Content-Type为什么App端上传没问题因为App端底层是原生HTTP请求uni-app的实现是自己拼接multipart请求体的它可能在你没有提供boundary时自动补上或者根本不依赖header里的boundary来做解析所以这个错误被掩盖了。而H5端是浏览器解析请求体浏览器只负责发后端解析失败就是失败没有任何兜底。那段经典错误代码长这样uni.uploadFile({ url: https://api.example.com/upload, filePath: tempFilePath, name: file, header: { Content-Type: multipart/form-data // 就是这一行 }, success: (res) { console.log(res); } });修复方式非常简单把header里的Content-Type删掉。只要你用的是FormData或者uni.uploadFile浏览器会自动生成带boundary的完整Content-Type完全不需要手动指定。如果你一定要设置header那就只加业务相关的自定义header比如Authorization。注意H5端上传时header里不要出现Content-Type这是第一条铁律。你写了后端大概率就400了。这个坑占了H5上传失败的三成以上。3. 文件路径与name字段H5端两个不起眼但致命的细节3.1 blob:临时路径不是App端的本地文件路径排除了Content-Type之后下一个高频问题出在文件本身。H5端用uni.chooseImage或uni.chooseFile选择文件后tempFilePaths[0]返回的是blob:http://localhost:8080/a1b2c3d4这种东西tempFiles[0]则是浏览器原生的File对象有name、size、type属性。我之前遇到过一种写法是从旧项目里复制出来的uni.chooseImage({ success: (res) { uni.uploadFile({ url: https://api.example.com/upload, filePath: res.tempFilePaths[0], name: file, success: () {} }); } });这段代码在App端可能没问题但在H5端有个隐患某些版本的uni-app在处理blob URL时如果内部转换失败或者File对象在FormData里没有被正确挂上去后端就会收到一个看似有文件字段、实际没有文件内容的请求。表现就是400或者后端收到的MultipartFile是空的。稳妥的做法有几个优先使用tempFiles里的File对象通过files参数传给uni.uploadFile。用tempFilePaths[0]配合filePath时先确认当前uni-app版本对H5端的支持情况。如果后端接口还需要额外的业务字段用formData传不要自己拼到url上。我实际用下来最稳的H5写法是这样uni.chooseImage({ count: 1, success: (chooseRes) { const tempFiles chooseRes.tempFiles; // H5端是File对象数组 uni.uploadFile({ url: https://api.example.com/upload, filePath: chooseRes.tempFilePaths[0], files: tempFiles, // 显式把File对象传进去 name: file, formData: { bizType: avatar }, success: (res) { console.log(上传成功, res.data); }, fail: (err) { console.error(上传失败, err); } }); } });3.2 前端name与后端接收参数对不上必出400如果说Content-Type和blob路径是请求不合法那name对不上就是字段找不到。前端name: file意味着文件在FormData里的字段名是file后端解析时也要按file来取。Spring Boot里常见两种写法// 写法一参数名和前端name一致 PostMapping(/api/upload) public R upload(RequestParam(file) MultipartFile file) { // 正常 } // 写法二参数名不一致但注解里指定了 PostMapping(/api/upload) public R upload(RequestParam(file) MultipartFile uploadFile) { // 正常注解告诉框架前端字段叫file } // 写法三完全对不上必报400 PostMapping(/api/upload) public R upload(RequestParam(uploadFile) MultipartFile file) { // 报错Required part uploadFile is not present }第三种写法如果请求里没有名为uploadFile的字段Spring MVC会直接抛MultipartException接口返回400。后端日志里会明确写Required part uploadFile is not present看到这句话不用怀疑就是名字对不上。还有一部分后端同学用的是RequestPart(file)这也是可以的但要注意前端那边不要设置Content-Type: application/json否则请求体不是multipartRequestPart解析也会失败。前后端联调时最好像约定接口协议一样把文件字段名、业务字段名写清楚。4. 后端与网关的默认限制400也可能是正常拒绝4.1 Spring Boot的multipart大小限制有时候前端和后端代码都写对了请求到了后端但后端拒绝了。最常见的就是上传大小超过了框架默认限制。Spring Boot 2.x里multipart的默认限制是max-file-size: 1MB单个文件最大1MBmax-request-size: 10MB整个请求体最大10MB也就是说你传一个2MB的头像图片在没有改配置的情况下Spring Boot会直接拒绝返回400。后端日志里大概率能看到MaxUploadSizeExceededException或者FileSizeLimitExceededException。如果你需要支持更大的文件在application.yml里加配置spring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB max-in-memory-size: 2MBmax-in-memory-size是文件在内存中的缓冲阈值超过这个值会临时写入磁盘不影响上传大小限制但会影响性能调优这里先不展开。改完配置后记得重启服务然后重新测。4.2 Nginx的client_max_body_size与网关层限制如果你的H5是部署到Nginx后面或者后端接口走了网关那还得检查这一层。Nginx默认的client_max_body_size只有1m超过1MB的请求Nginx直接返回413而不是400但有些配置场景下比如和后端交互异常表现也可能被包装成400。建议在Nginx配代理的location里显式加大location /api/ { client_max_body_size 20m; proxy_pass http://backend-server; proxy_set_header Host $host; }这里有个容易漏的点client_max_body_size放在http、server、location级别都有效但如果你只改到location别的location仍然继承默认1m。如果项目里有多个上传接口分散在不同路径最好在server级别统一设置避免漏配。网关层也是一样的道理比如你用了Kong、APISIX这类网关内部的请求体大小限制也要同步调大否则Nginx过了、网关又给你卡掉排查起来很费劲。5. 跨域预检与过滤器最后一个容易误判的环节5.1 OPTIONS预检请求的来龙去脉很多H5和前后端分离项目是分端口跑的前端跑在5173后端跑在8080跨域问题避不开。关于跨域大多数人只关注后端要配CORS但容易忽略预检请求。浏览器跨域时如果请求不是简单请求会先发一个OPTIONS请求探路服务端响应了合适的CORS头后浏览器才发真正的POST请求。什么是非简单请求比如你上传时header里带了自定义字段Authorization或X-Token或者Content-Type不是那三种简单值都会触发预检。问题在于很多后端的登录拦截器或者校验过滤器把OPTIONS请求也拦下来了返回401或400。浏览器拿不到正常的CORS响应上传请求就会失败表现为Network面板里出现一个OPTIONS请求状态是400或失败然后真正的上传请求根本没发出去。判断方法很简单打开Chrome DevTools的Network面板如果看到上传请求前有一个OPTIONS请求而且它红了那一半以上是预检没过。5.2 后端CORS配置的常见翻车点Spring Boot配置跨域常见的写法是Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }有几个翻车点要注意allowedMethods里必须包含OPTIONS很多教程只写了GET、POST预检请求就过不去。如果项目里有自定义HandlerInterceptor要注意放行OPTIONS请求别让拦截器把预检拦了。用了Spring Security的项目跨域配置和Security的过滤器顺序也有讲究配置不当会出现预检放行了但请求头不一致又失败的情况。如果是Node.js后端用express配合cors中间件默认会处理OPTIONS相对省心const cors require(cors); app.use(cors());但如果你也写了自定义header白名单记得把上传需要的那几个头加进去。6. 完整复现一次从报错到修复的排查路径前面把常见原因都讲了一遍这一章我按真实排错的过程走一遍模拟一下遇到400时应该怎么一步步定位。6.1 第一轮看Network面板锁定请求长什么样假设我在做一个uni-app H5项目上传头像功能报400。我第一步永远是打开Chrome DevTools的Network找到那条红色的上传请求点开看Headers。先看Request Headers里的Content-Type如果显示的是Content-Type: multipart/form-data而不是Content-Type: multipart/form-data; boundary----WebKitFormBoundary7MA4YWxkTrZu0gW那基本可以断定是header手动写了Content-Typeboundary丢了。这就是第二章说的那个坑直接改代码删掉header里的Content-Type。再看Request Payload或Form Data区域。如果这里能看到file: (binary)这样的字段说明文件已经拼上去了如果只有业务字段没有文件字段或者文件字段显示的是(no content)问题出在文件路径或File对象传递上回到第三章的写法调整。6.2 第二轮看后端日志让后端同学给你报关键词如果Content-Type是正常的有boundary文件也在但接口还是400这时候别猜了让后端把日志拉出来看。不同关键词对应不同原因后端日志关键词原因处理方向Required part xxx is not present前端name和后端参数名对不上统一字段名Current request is not a multipart request请求体不是合法的multipart检查Content-Type和boundaryMaxUploadSizeExceededException超过大小限制调大Spring Boot或Nginx限制Failed to parse multipart servlet requestmultipart解析失败检查文件是否损坏、请求是否被截断日志里如果是Required part file is not present前端和后端先对一下name字段我遇到过不少次前端叫file、后端注解写uploadFile的情况一改就好。6.3 第三轮复现排除跨域和大小的干扰如果日志没有明显报错但请求就是失败我一般会把问题收窄。用一个最小的测试请求不带自定义header只传一个小文件几十KB那种看能不能成功。小文件成功、大文件失败就是大小限制查Nginx和后端配置。不带header成功、带header失败跨域预检问题查OPTIONS和CORS配置。浏览器直连后端成功、走完整链路失败中间有网关或代理层限制。这一轮的目的是把问题从所有可能性收窄到某一层我见过有人为了一个400折腾了一整天结果只是Nginx的client_max_body_size没改很冤枉。6.4 最终修复与复原验证假设我这次遇到的是Content-Typeboundary问题修复后我会按这个顺序验证清掉浏览器缓存刷新页面。重新选择文件上传打开Network确认Content-Type里带上了boundary。确认请求成功返回后端日志显示文件字段非空。再换一个稍大的文件比如5MB测试验证大小限制是否适合业务。如果是name字段问题修复后看后端日志不再是Required part。整个过程不要批量改代码改一处验证一处尤其是前端、后端不是同一个人维护的时候每轮修复都要让两边各自确认。7. 建议直接照抄的前后端上传代码模板7.1 uni-app前端侧的标准写法下面这套写法我用了很久H5端和App端都能跑后端不用区分端uploadFile( tempFilePath, tempFiles, extraData ) { return new Promise((resolve, reject) { uni.uploadFile({ url: https://api.example.com/api/upload, filePath: tempFilePath, files: tempFiles, name: file, formData: extraData || {}, success: (res) { try { const data typeof res.data string ? JSON.parse(res.data) : res.data; resolve(data); } catch (e) { resolve(res.data); } }, fail: (err) { reject(err); } }); }); }调用的时候uni.chooseImage({ count: 1, success: (res) { const tempFilePath res.tempFilePaths[0]; const tempFiles res.tempFiles; this.uploadFile(tempFilePath, tempFiles, { bizType: avatar }) .then((res) { console.log(上传成功, res); }) .catch((err) { console.error(上传失败, err); }); } });核心就两条不手动设置Content-Type有File对象就通过files传进去。如果遇到的是H5端特殊版本导致的blob URL转换问题直接传files数组可以绕开大部分坑。7.2 Spring Boot和Node侧的标准写法后端如果用的是Spring Boot接收代码建议这样写PostMapping(/api/upload) public R upload(RequestParam(file) MultipartFile file, RequestParam(value bizType, required false) String bizType) { if (file.isEmpty()) { return R.fail(file is empty); } // 你的业务处理 return R.ok(); }配合application.ymlspring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB注意file.isEmpty()这个判断有时前端确实把请求发出去了但文件内容是空的这个判断能帮你快速定位而不是等到后面处理时才发现。如果是Node.js后端express配合multer标准写法const multer require(multer); const upload multer({ dest: uploads/ }); app.post(/api/upload, upload.single(file), (req, res) { if (!req.file) { return res.status(400).json({ message: no file }); } res.json({ filename: req.file.filename }); });multer的upload.single(file)里的file要和前端的name一致。如果前端用uni.uploadFile传的是name: file这里就是single(file)。7.3 验证清单前后端对齐时逐项打勾最后给一份我在项目里给前后端联调用清单照着一项项打勾能省掉大量互相扯皮的时间[ ] 前端header里没有手动设置Content-Type[ ] 前端name字段和后端接收参数名一致[ ] H5端选择了文件后tempFiles里的File对象正常传递[ ] 后端multipart大小限制符合业务需求Spring Boot配置、Nginx的client_max_body_size[ ] 后端CORS配置包含OPTIONS且拦截器放行预检请求[ ] 后端接口在日志里能看到文件字段不存在Required part报错[ ] 跨域环境下上传成功后响应头里能看到正确的CORS头我自己踩过几轮之后现在定了个规矩凡是H5上传前端不手写Content-Type后端统一用RequestParam(file)接遇到400先看Network再看后端日志而不是一遍遍重新编译、瞎试。这个领域的问题九成都是上面这几个原因只要你按着前面的链路逐层排查一般十几分钟就能定位没必要把它当成什么玄学问题。
返回列表