Reputation: 1352
Swift 5, Xcode Version 10.2.1 (10E1001)
Hi everyone, I'd appreciate any help on this.
I'm creating a call to post an attachment (PNG) to my POST call. I'm making the call to ServiceNow. If I use the same keys in the body as PostMan the call in Postman works fine. However, the below seems like it's having a hard time with the attachment. The image in this example is PNG asset.
For comparison, if I omit the attachment in Postman, I get the exact same error message. I believe the image isn't be properly formatted...
Thanks in advance...
I get the following error from ServiceNow:
{
error = {
detail = "<null>";
message = "Failed to create the attachment. File part might be missing in the request.";
};
status = failure;
}
And this is my code:
func createDataBody() -> Data {
let newLine = "\r\n"
let twoNewLines = newLine + newLine
let boundary = "----------------------------\(UUID().uuidString)" + newLine
var body = Data()
let stringEncoding = String.Encoding.utf16
body.append(boundary.data(using: stringEncoding)!)
let table_name = "Content-Disposition: form-data; name=\"table_name\"" + twoNewLines
body.append(table_name.data(using: stringEncoding)!)
//incident
body.append("incident".data(using: stringEncoding)!)
//new line
body.append(newLine.data(using: stringEncoding)!)
//boundary
body.append(boundary.data(using: stringEncoding)!)
let table_sys_id = "Content-Disposition: form-data; name=\"table_sys_id\"" + twoNewLines
body.append(table_sys_id.data(using: stringEncoding)!)
//ba931ddadbf93b00f7bbdd0b5e96193c
body.append("ba931ddadbf93b00f7bbdd0b5e96193c".data(using: stringEncoding)!)
//new line
body.append(newLine.data(using: stringEncoding)!)
//boundary
body.append(boundary.data(using: stringEncoding)!)
let file = "Content-Disposition: form-data; name=\"file\"; filename=\"[email protected]\"" + newLine
body.append(file.data(using: stringEncoding)!)
let type = "Content-Type: image/png" + twoNewLines
body.append(type.data(using: stringEncoding)!)
//new line
body.append(newLine.data(using: stringEncoding)!)
let img = #imageLiteral(resourceName: "Artboard@1x")
if let fileContent = img.pngData() {
body.append(fileContent)
}
//new line
body.append(newLine.data(using: stringEncoding)!)
body.append("--\(UUID().uuidString)--".data(using: stringEncoding)!)
print(String(data: body, encoding: .utf16)!)
return body
}
Here is what the body look like, with the image data omitted:
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36
Content-Disposition: form-data; name="table_name"
incident
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36
Content-Disposition: form-data; name="table_sys_id"
ba931ddadbf93b00f7bbdd0b5e96193c
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36
Content-Disposition: form-data; name="file"; filename="[email protected]"
Content-Type: image/png
.....
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36
Here is the header call
func addAttachmentToIncident() {
let passwordString = "\(userNameTextField.text!):\(passwordTextField.text!)"
let passwordData = passwordString.data(using: String.Encoding.utf8)
let base64EncodedCredential = passwordData?.base64EncodedString(options: Data.Base64EncodingOptions.lineLength76Characters)
let boundary = generateBoundaryString()
let headers = [
"authorization": "Basic " + base64EncodedCredential!,
"cache-control": "no-cache",
"Accept": "application/json",
"content-type": "multipart/form-data; boundary=--\(boundary)"
]
guard let url = URL(string: "https://xxx.service-now.com/api/now/attachment/upload") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
let dataBody = createDataBody(boundary: boundary)
request.httpBody = dataBody
let session = URLSession.shared
session.dataTask(with: request) { (data, response, error) in
if let response = response {
print(response)
}
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print(json)
} catch {
print(error)
}
}
}.resume()
} //addAttachmentToIncident
Upvotes: 0
Views: 2445
Reputation: 438467
A couple of observations:
The final boundary is not correct. Assuming you’ve created a boundary that starts with --
, you should be appending \(boundary)--
as the final boundary. Right now the code is creating a new UUID (and omitting all of those extra dashes you added in the original boundary), so it won’t match the rest of the boundaries. You need a newLine
sequence after that final boundary, too.
The absence of this final boundary could be preventing it from recognizing this part of the body, and thus the “File part might be missing” message.
The boundary
should not be a local variable. When preparing multipart requests, you have to specify the boundary in the header (and it has to be the same boundary here, not another UUID()
instance).
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
Generally, I would have the caller create the boundary, use that when creating the request header, and then pass the boundary as a parameter to this method. See Upload image with parameters in Swift.
The absence of the same boundary value in the header and the body would prevent it from recognizing any of these parts of the body.
You have defined your local boundary to include the newLine
. Obviously, it shouldn’t be local var at all, but it must not include newline at the end, otherwise the attempt to append the last boundary of /(boundary)--
will fail.
Obviously, if you take this out of the boundary, make sure to insert the appropriate newlines as you build the body, where needed, though. Bottom line, make sure your body looks like the following (with the final --
):
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36
Content-Disposition: form-data; name="table_name"
incident
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36
Content-Disposition: form-data; name="table_sys_id"
ba931ddadbf93b00f7bbdd0b5e96193c
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36
Content-Disposition: form-data; name="file"; filename="[email protected]"
Content-Type: image/png
.....
----------------------------F2152BF1-CE54-4E86-B8D0-931FA36F7C36--
In their curl
example for /now/attachment/upload
, they are using a field name of uploadFile
, but you are using file
. You may want to double check your field name and match the curl
and postman examples.
curl "https://instance.service-now.com/api/now/attachment/upload" \
--request POST \
--header "Accept:application/json" \
--user "'admin':'admin'" \
--header "Content-Type:multipart/form-data" \
-F 'table_name=incident' \
-F 'table_sys_id=d71f7935c0a8016700802b64c67c11c6' \
-F '[email protected]'
If, after fixing the above, it still doesn’t work, I’d suggest you use Charles or Wireshark and compare a successful request vs the one you’re generating programmatically.
Needless to say, you might want to consider using Alamofire, which gets you out of the weeds of creating well-formed multipart requests.
Upvotes: 1