You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:06:19