Paul S.
Paul S.

Reputation: 1352

Swift adding attachment for multipart/form-data POST

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

Answers (1)

Rob
Rob

Reputation: 438467

A couple of observations:

  1. 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.

  2. 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.

  3. 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--
    
  4. 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

Related Questions