Swift发送multipart/form-data POST请求空数据报错排查
问题定位
你的代码有6个核心问题直接导致请求失败:
- 整型字段传输逻辑完全错误:multipart/form-data 中除了文件/二进制资源外,所有普通字段(包括数值类型)都是以纯文本形式传输的,不需要把整型变量的内存二进制数据封装为Data上传。你自定义的
mimeType: "Int"不属于标准MIME类型,后端解析到这类非法字段会直接拒绝请求,返回空响应。 - 图片MIME类型配置错误:你上传的是PNG格式图片(调用了
pngData()),却写了无效的img/jpeg,正确的PNG MIME为image/png,JPG的MIME为image/jpeg。 - 表单结束边界格式不规范:multipart 协议要求结束边界必须以
--{boundary}--\r\n结尾,你的代码遗漏了末尾的\r\n换行符,对格式校验严格的后端会直接判定表单格式非法。 - 字段拼写错误:你定义的商品描述字段名为
producDescription,漏写了字母t,和常规后端约定的productDescription不匹配,会导致后端拿不到对应参数。 - 错误处理逻辑有缺陷:你在网络回调中直接guard判定data非空才继续执行,当后端返回错误状态码且响应体为空时会直接退出,拿不到真实的错误上下文;同时你对可选值
self.image?.pngData()直接强制解包,图片未选中时会直接触发崩溃。 - 冗余代码:你给
NSMutableData写了两个功能完全一致的扩展方法,属于无效重复代码。
修正后的原生URLSession实现
首先清理冗余代码、补全边界格式、新增数值字段快捷方法,修正后的MultipartFormDataRequest代码如下:
struct MultipartFormDataRequest { private let boundary: String = "Boundary-\(UUID().uuidString)" private var httpBody = NSMutableData() let url: URL init(url: URL) { self.url = url } // 普通文本字段 mutating func addTextField(named name: String, value: String) { httpBody.append(textFormField(named: name, value: value)) } // 数值字段快捷方法,自动转字符串 mutating func addNumberField(named name: String, value: Any) { httpBody.append(textFormField(named: name, value: "\(value)")) } private func textFormField(named name: String, value: String) -> String { var fieldString = "--\(boundary)\r\n" fieldString += "Content-Disposition: form-data; name=\"\(name)\"\r\n" fieldString += "Content-Type: text/plain; charset=utf-8\r\n" fieldString += "\r\n" fieldString += "\(value)\r\n" return fieldString } // 文件/二进制资源字段 mutating func addDataField(named name: String, fileName: String, data: Data, mimeType: String) { httpBody.append(dataFormField(named: name, fileName: fileName, data: data, mimeType: mimeType)) } private func dataFormField(named name: String, fileName: String, data: Data, mimeType: String) -> Data { let fieldData = NSMutableData() fieldData.append("--\(boundary)\r\n") fieldData.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n") fieldData.append("Content-Type: \(mimeType)\r\n") fieldData.append("\r\n") fieldData.append(data) fieldData.append("\r\n") return fieldData as Data } func asURLRequest() -> URLRequest { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") // 修正结束边界格式 httpBody.append("--\(boundary)--\r\n") request.httpBody = httpBody as Data return request } } extension NSMutableData { func append(_ string: String) { if let data = string.data(using: .utf8) { self.append(data) } } } extension URLSession { func dataTask( with request: MultipartFormDataRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void ) -> URLSessionDataTask { return dataTask(with: request.asURLRequest(), completionHandler: completionHandler) } }
对应的业务上传方法修正如下:
func uploadForm() { guard let imageData = self.image?.pngData() else { print("请先选择图片") return } guard let backendUrl = URL(string: "替换为你的后端实际地址") else { print("后端地址非法") return } // 固定整型值直接传,可选ID先做解包 let categoryId = 1 let discountId = 1 let shopId = 1 let tagId = 1 guard let userId = CreateAccountAuth.shared.userDetails?.id else { print("用户未登录") return } var request = MultipartFormDataRequest(url: backendUrl) // 图片字段,传正确的MIME和文件名 request.addDataField(named: "product_image1", fileName: "product.png", data: imageData, mimeType: "image/png") // 文本字段,修正字段名拼写 request.addTextField(named: "productName", value: self.artworkTitle) request.addTextField(named: "productDescription", value: self.artworkDescription) // 数值字段直接用快捷方法传,不需要转二进制 request.addNumberField(named: "metauserID", value: userId) request.addNumberField(named: "product_categoryID", value: categoryId) request.addNumberField(named: "discount_ID", value: discountId) request.addNumberField(named: "shop_ID", value: shopId) request.addNumberField(named: "boostTagsID", value: tagId) let task = URLSession.shared.dataTask(with: request.asURLRequest()) { data, response, error in // 先处理网络层错误 if let error = error { print("网络请求错误:\(error.localizedDescription)") return } guard let httpResp = response as? HTTPURLResponse else { print("无效的响应") return } print("响应状态码:\(httpResp.statusCode)") // 处理响应数据,空数据直接返回不做JSON解析 guard let respData = data, !respData.isEmpty else { print("响应数据为空") return } do { let jsonResp = try JSONSerialization.jsonObject(with: respData) print("响应内容:\(jsonResp)") if (200...299).contains(httpResp.statusCode) { print("上传成功") } else { print("上传失败,状态码异常") } } catch { print("响应解析失败:\(error.localizedDescription)") } } task.resume() }
Alamofire 实现方案
Alamofire 对multipart上传做了封装,不需要自己拼接边界,数值类型直接转字符串传输即可,示例代码如下:
import Alamofire func uploadWithAlamofire() { guard let imageData = self.image?.pngData() else { print("请先选择图片") return } guard let userId = CreateAccountAuth.shared.userDetails?.id else { print("用户未登录") return } let parameters: [String: String] = [ "productName": self.artworkTitle, "productDescription": self.artworkDescription, "metauserID": "\(userId)", "product_categoryID": "1", "discount_ID": "1", "shop_ID": "1", "boostTagsID": "1" ] AF.upload( multipartFormData: { formData in // 追加图片字段 formData.append(imageData, withName: "product_image1", fileName: "product.png", mimeType: "image/png") // 追加普通文本/数值字段 for (key, value) in parameters { formData.append(value.data(using: .utf8)!, withName: key) } }, to: "替换为你的后端实际地址" ).response { resp in switch resp.result { case .success(let data): guard let data = data, !data.isEmpty else { print("响应为空") return } do { let json = try JSONSerialization.jsonObject(with: data) print("响应:\(json)") } catch { print("解析失败:\(error)") } case .failure(let error): print("请求失败:\(error.localizedDescription)") } } }
注意:所有字段名、字段值类型必须和后端接口文档完全一致,避免出现拼写、类型不匹配的问题。
内容的提问来源于stack exchange,提问作者AadityaS
相关产品推荐
相关产品推荐

