Swift中的HTTP POST请求畸形的Form-Data主体?
我正在编写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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论