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

Swift Alamofire上传multipartFormData报400错误排查求助

Alamofire MultipartFormData 400错误排查

我尝试用Alamofire发送multipartFormData表单数据,多次尝试后始终返回400错误。postId和userId由服务器生成,无需在上传参数中传递,Endpoint配置确认无误。以下是相关代码及服务器接收的Postman示例,请求协助排查问题。

服务器接收的Postman JSON示例

{
    "pTostId": null,
    "userId": 81,
    "createdAt": null,
    "modifiedAt": null,
    "startedAt": "2022-07-29",
    "endedAt": "2023-07-28",
    "title": "시연영상2",
    "memo": "굿",
    "category": "Animation",
    "views": 1,
    "genre": "액션2",
    "platform": "2",
    "emoji": "1",
    "comment": "시연영상 3저장",
    "image": "https://servername-s3.s3.ap-northeast-2.amazonaws.com/4cb5bbf5-064d-4bb2-a7aa-db7cfa999850-Untitled%20design.png"
}

调用代码(AddCardView)

struct AddCardView: View {
    @ObservedObject var viewModel = CardViewModel.shared

    @State private var image: UIImage? = nil
    @State private var card = Card()

    @State private var isShowingImagePicker = false
    @Environment(\.colorScheme) var colorScheme
    @State private var presentAlert = false
    
    var body: some View {
        VStack(spacing: 20) {
            TextField("Title", text: Binding<String>(
                get: { self.card.title ?? "" },
                set: { self.card.title = $0 }
            ))
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)

            TextField("Comment", text: Binding<String>(
                get: { self.card.comment ?? "" },
                set: { self.card.comment = $0 }
            ))
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)

            if let selectedImage = image {
                Image(uiImage: selectedImage)
                    .resizable()
                    .frame(width: 200, height: 200)
            }

            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 280, height: 400)
                    .cornerRadius(20)
                    .clipped()
                    .onTapGesture {
                        isShowingImagePicker = true
                    }
            } else {
                Image(systemName: "camera")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 100, height: 250)
                    .onTapGesture {
                        isShowingImagePicker = true
                    }
            }

            Button(action: {
                viewModel.addCard(image: image, with: card)
            }) {
                Text("Add Card")
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
        .padding()
        .sheet(isPresented: $isShowingImagePicker) {
            ImagePicker(image: $image)
        }
    }
}

Card结构体

struct Card: Codable, Identifiable {
    var id: Int? { postId }
    let postId: Int?
    let userId: Int?
    
    var image: String?
    var comment: String?
    var title: String?
    var emoji: String?
    var platform: String?
    var genre: String?
    var views: Int?
    var memo: String?
    var category: CardCategory?
    
    var createdAt: String?
    var modifiedAt: String?
    var startedAt: String?
    var endedAt: String?
    
    var encodedImageURL: URL? {
        return image?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed).flatMap { URL(string: $0) }
    }
    
    init() {
        self.postId = nil
        self.userId = nil
        self.image = nil
        self.comment = nil
        self.title = nil
        self.emoji = nil
        self.platform = nil
        self.genre = nil
        self.views = nil
        self.memo = nil
        self.category = nil
        self.createdAt = nil
        self.modifiedAt = nil
        self.startedAt = nil
        self.endedAt = nil
    }
}

APIClient实现

func addCard(image: UIImage?, with model: Card,  completionHandler: @escaping () -> Void) {
    let url = APIEndpoint.addCardURL
    let header: HTTPHeaders = ["Content-Type": "multipart/form-data"]
    
    let parameters = [
        "startedAt" : model.startedAt ?? "No startedAt",
        "endedAt" : model.endedAt ?? "No endedAt",
        "title" : model.title ?? "No Title",
        "memo" : model.memo ?? "No memo",
        "category" : model.category ?? "Animation",
        "views" : model.views ?? 0,
        "genre" : model.genre ?? "No genre",
        "platform" : model.platform ?? "No platform",
        "emoji" : model.emoji ?? "No emoji",
        "comment" : model.comment ?? "No comment"
    ] as [String : Any]
    
    AF.upload(multipartFormData: { multipartFormData in
        for (key, value) in parameters {
            multipartFormData.append("\(value)".data(using: .utf8)!, withName: key, mimeType: "text/plain")
        }
        let imageData = image?.pngData() ?? Data()
        multipartFormData.append(imageData, withName: "image", fileName: "image.png", mimeType: "image/png")
    }, to: url, method: .post, headers: header)
    .responseData { response in
        guard let statusCode = response.response?.statusCode else { return }
        switch statusCode {
        case 200...299:
            print("카드 등록 성공")
            completionHandler()
        case 300...399:
            print("Redirection messages")
        case 400:
            print("400")
        case 401...499:
            print("Client error responses")
        case 500...600:
            print("Server error responses")
        default:
            print("카드 등록 실패")
        }
    }
}

CardViewModel代码

class CardViewModel: ObservableObject {
    static let shared = CardViewModel()
    @Published var cards: [Card] = []
    @Published var currentCard: Card?
    private init() {}
    private var apiClient = APIClient()
    func addCard(image: UIImage?, with model: Card) {
        apiClient.addCard(image: image, with: model) {
            DispatchQueue.main.async {
                self.cards.append(model)
                print("Card added successfully!")
            }
        }
    }
}

排查方向建议

  • 参数类型不匹配:Postman中views是整数,但代码里把它转为字符串传递,服务器可能期望整数格式。需确认服务器接收类型,若为整数,直接传递Int类型而非转字符串。
  • 默认值无效:部分参数用"No xxx"作为默认值(比如startedAt),但服务器期望合法日期格式,这类默认值会导致参数无效。需确认字段是否必填,必填则需用户输入合法值。
  • Content-Type头错误:Alamofire上传multipartFormData时会自动生成带boundary的Content-Type,手动设置multipart/form-data会覆盖自动生成的头,导致服务器无法解析表单。建议移除手动设置的该头。
  • CardCategory枚举处理:category是CardCategory?类型,转字符串时需确保枚举原始值与服务器期望一致(比如"Animation"),否则会传递错误值。
  • 空图片数据:未选图片时传递空Data给image字段,服务器可能不接受空文件,需确认是否允许图片为空,不允许则添加图片选择校验。
  • 抓包对比:用Charles抓包对比Postman和Alamofire的请求,检查参数名、类型、格式是否完全一致,同时在APIClient中打印请求日志辅助排查。

内容的提问来源于stack exchange,提问作者YUNG LEE

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 10:44:50