Swift中的HTTP POST请求畸形的Form-Data主体?

发布于 2025-02-13 15:05:39 字数 7041 浏览 0 评论 0原文

我正在编写HTTP POST请求,以通过Facebook Graph API上传媒体(图片)将其上传到WhatsApp,并在其文档中格式化如下:

curl -X POST \
  'https://graph.facebook.com/v13.0/FROM_PHONE_NUMBER_ID/media' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -F 'file=@/local/path/file.jpg;type=image/jpeg' 
  -F 'messaging_product=whatsapp'

我有正确的信息,因为它在构造Postman的请求并保留时可以工作身体为形式。

这是Postman成功请求的原始日志(我已经删除了访问令牌,边界字符串和图像的二进制文件):

POST /v13.0/111101394989431/media HTTP/1.1
Authorization: Bearer <token>
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: d84bb595-a5a5-4f42-b286-46dfef5828e5
Host: graph.facebook.com
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=<boundary-string>
Content-Length: 65306
--<boundary-string>
Content-Disposition: form-data; name="messaging_product"

whatsapp
--<boundary-string>
Content-Disposition: form-data; name="file"; filename="gm.jpeg"

<gm.jpeg>
--<boundary-string>--

这是Swift中的我的网络代码,为您的利益发表了评论。请查看CreateBody方法(在底部),这可能是格式较差的HTTP主体的来源,因此是400个错误。

//
//  WhatsappMediaAPIClient.swift
//  SnapShare
//
//  Created by Faraz Malik on 04/07/2022.
//

import Foundation
import UIKit

enum WhatsappImageError: Error {
    case requestFailed
    case responseUnsuccessful(statusCode: Int)
    case jsonParsingFailure
    case invalidURL
}

class WhatsappImageAPIClient {
    private let whatsappPhoneID = "<phone-ID>"
    
    // returns the base url string with the phone ID attached, when accessed
    private lazy var baseUrlString: String = {
        return "https://graph.facebook.com/v13.0/\(whatsappPhoneID)/media"
    }()
    
    let decoder = JSONDecoder()
    let session: URLSession
    
    init(withConfiguration configuration: URLSessionConfiguration = .default) {
        self.session = URLSession(configuration: configuration)
    }
    
    // MARK: Typealiases
    
    typealias WhatsappImageCompletionHandler = (WhatsappImage?, Error?) -> Void
    
    
    // MARK: Post Whatsapp Image
    // called to upload image to WhatsApp via Facebook Graph API
    // calls escaping completion handler to pass up data or error appropriately.
    public func postWhatsappImage(withAccessToken accessToken: String, imageData: Data, completionHandler completion: @escaping WhatsappImageCompletionHandler) {
        
        // creates URL from URL string
        guard let url = URL(string: baseUrlString) else {
            completion(nil, WhatsappImageError.invalidURL)
            return
        }
        
        // gets a boundary string to work with for this request
        let boundaryString = getBoundaryString()
        
        // creates a request
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        
        // adds access token and content type as fields in the header
        request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundaryString)", forHTTPHeaderField: "Content-Type")
        
        // required parameter for form-data in body
        let parameters = ["messaging_product": "whatsapp", "type": "image/jpeg"]
        
        // gets and assigns body, formatted to form-data with parameters and image data
        let requestData = createBody(withParameters: parameters, imageData: imageData, boundaryString: boundaryString)
        request.httpBody = requestData
        
        // sends request asynchronously (i.e. background)
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                // if we got a response
                if let data = data {
                    // if we can cast response as HTTPURLResponse
                    guard let httpResponse = response as? HTTPURLResponse else {
                        completion(nil, WhatsappImageError.requestFailed)
                        return
                    }
                    
                    // if it's ok
                    if httpResponse.statusCode == 200 {
                        do {
                            // try to decode it
                            let whatsappImageID = try self.decoder.decode(WhatsappImage.self, from: data)
                            // pass up to completion handler
                            completion(whatsappImageID, nil)
                        } catch {
                            // pass up to completion handler
                            completion(nil, WhatsappImageError.jsonParsingFailure)
                        }
                    } else if httpResponse.statusCode == 400 {
                        // print message for inspection
                        print("\n\n\n\n")
                        print(String(decoding: data, as: UTF8.self))
                        print("\n\n\n\n")
                        // pass up to completion handler
                        completion(nil, WhatsappImageError.responseUnsuccessful(statusCode: httpResponse.statusCode))
                    } else {
                        // pass up to completion handler
                        completion(nil, WhatsappImageError.responseUnsuccessful(statusCode: httpResponse.statusCode))
                    }
                    
                } else if let error = error {
                    // pass up error
                    completion(nil, error)
                }
            }
        }
        
        task.resume()
    }
    
    // creates body data (error could very well lie here because 400 bad request means body is malformed)
    // please give this a look (image is always jpeg)
    private func createBody(withParameters parameters: [String: String], imageData: Data, boundaryString: String) -> Data {
        
        let body = NSMutableData()
        let boundaryPrefix = "--\(boundaryString)\r\n"
        let filename = "upload.jpg"
                
        for (key, value) in parameters {
            body.append(boundaryPrefix.data(using: .utf8)!)
            body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n".data(using: .utf8)!)
            body.append("\(value)\r\n".data(using: .utf8)!)
        }
        
        body.append(boundaryPrefix.data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n".data(using: .utf8)!)
        body.append("--".appending(boundaryString.appending("--\r\n")).data(using: .utf8)!)
        
        return body as Data
    }
    
    private func getBoundaryString() -> String {
        let boundary = "\(UUID().uuidString)"
        return boundary
    }
}

希望您能发现出了什么问题,因为我已经使用了2天了,而与Postman相比,我无法弄清楚我在发送Swift请求时做的不同之处 - 使它失败的差异。

I'm writing a HTTP POST request for uploading media (images here) to WhatsApp via the Facebook Graph API, formatted as follows in their documentation:

curl -X POST \
  'https://graph.facebook.com/v13.0/FROM_PHONE_NUMBER_ID/media' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -F 'file=@/local/path/file.jpg;type=image/jpeg' 
  -F 'messaging_product=whatsapp'

I've got the information right, because it works when structuring the request from Postman, and keeping the body as form-data.

Here's the raw log from Postman for the successful request (I've removed the access token, the boundary string and the binaries for the image):

POST /v13.0/111101394989431/media HTTP/1.1
Authorization: Bearer <token>
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: d84bb595-a5a5-4f42-b286-46dfef5828e5
Host: graph.facebook.com
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=<boundary-string>
Content-Length: 65306
--<boundary-string>
Content-Disposition: form-data; name="messaging_product"

whatsapp
--<boundary-string>
Content-Disposition: form-data; name="file"; filename="gm.jpeg"

<gm.jpeg>
--<boundary-string>--

Here's my networking code in Swift, commented for your benefit. Please check out the createBody method (at bottom), which is likely the source of the poorly formatted HTTP body and thus 400 error.

//
//  WhatsappMediaAPIClient.swift
//  SnapShare
//
//  Created by Faraz Malik on 04/07/2022.
//

import Foundation
import UIKit

enum WhatsappImageError: Error {
    case requestFailed
    case responseUnsuccessful(statusCode: Int)
    case jsonParsingFailure
    case invalidURL
}

class WhatsappImageAPIClient {
    private let whatsappPhoneID = "<phone-ID>"
    
    // returns the base url string with the phone ID attached, when accessed
    private lazy var baseUrlString: String = {
        return "https://graph.facebook.com/v13.0/\(whatsappPhoneID)/media"
    }()
    
    let decoder = JSONDecoder()
    let session: URLSession
    
    init(withConfiguration configuration: URLSessionConfiguration = .default) {
        self.session = URLSession(configuration: configuration)
    }
    
    // MARK: Typealiases
    
    typealias WhatsappImageCompletionHandler = (WhatsappImage?, Error?) -> Void
    
    
    // MARK: Post Whatsapp Image
    // called to upload image to WhatsApp via Facebook Graph API
    // calls escaping completion handler to pass up data or error appropriately.
    public func postWhatsappImage(withAccessToken accessToken: String, imageData: Data, completionHandler completion: @escaping WhatsappImageCompletionHandler) {
        
        // creates URL from URL string
        guard let url = URL(string: baseUrlString) else {
            completion(nil, WhatsappImageError.invalidURL)
            return
        }
        
        // gets a boundary string to work with for this request
        let boundaryString = getBoundaryString()
        
        // creates a request
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        
        // adds access token and content type as fields in the header
        request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundaryString)", forHTTPHeaderField: "Content-Type")
        
        // required parameter for form-data in body
        let parameters = ["messaging_product": "whatsapp", "type": "image/jpeg"]
        
        // gets and assigns body, formatted to form-data with parameters and image data
        let requestData = createBody(withParameters: parameters, imageData: imageData, boundaryString: boundaryString)
        request.httpBody = requestData
        
        // sends request asynchronously (i.e. background)
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                // if we got a response
                if let data = data {
                    // if we can cast response as HTTPURLResponse
                    guard let httpResponse = response as? HTTPURLResponse else {
                        completion(nil, WhatsappImageError.requestFailed)
                        return
                    }
                    
                    // if it's ok
                    if httpResponse.statusCode == 200 {
                        do {
                            // try to decode it
                            let whatsappImageID = try self.decoder.decode(WhatsappImage.self, from: data)
                            // pass up to completion handler
                            completion(whatsappImageID, nil)
                        } catch {
                            // pass up to completion handler
                            completion(nil, WhatsappImageError.jsonParsingFailure)
                        }
                    } else if httpResponse.statusCode == 400 {
                        // print message for inspection
                        print("\n\n\n\n")
                        print(String(decoding: data, as: UTF8.self))
                        print("\n\n\n\n")
                        // pass up to completion handler
                        completion(nil, WhatsappImageError.responseUnsuccessful(statusCode: httpResponse.statusCode))
                    } else {
                        // pass up to completion handler
                        completion(nil, WhatsappImageError.responseUnsuccessful(statusCode: httpResponse.statusCode))
                    }
                    
                } else if let error = error {
                    // pass up error
                    completion(nil, error)
                }
            }
        }
        
        task.resume()
    }
    
    // creates body data (error could very well lie here because 400 bad request means body is malformed)
    // please give this a look (image is always jpeg)
    private func createBody(withParameters parameters: [String: String], imageData: Data, boundaryString: String) -> Data {
        
        let body = NSMutableData()
        let boundaryPrefix = "--\(boundaryString)\r\n"
        let filename = "upload.jpg"
                
        for (key, value) in parameters {
            body.append(boundaryPrefix.data(using: .utf8)!)
            body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n".data(using: .utf8)!)
            body.append("\(value)\r\n".data(using: .utf8)!)
        }
        
        body.append(boundaryPrefix.data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n".data(using: .utf8)!)
        body.append("--".appending(boundaryString.appending("--\r\n")).data(using: .utf8)!)
        
        return body as Data
    }
    
    private func getBoundaryString() -> String {
        let boundary = "\(UUID().uuidString)"
        return boundary
    }
}

Hopefully you can spot what is going wrong, because I've been at this for 2 days and I can't figure out what I'm doing differently when sending the POST request from Swift compared to Postman - the difference that's making it fail.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文