Search code examples
vbaapirestms-accessxmlhttprequest

VBA TalentLMS API Post Request with multipart/form-data throwing error when passing Request Body with boundaries in as a string parameter


I am creating a Module in MS Access to make API calls to different endpoints in the TalentLMS API. I am creating functions to minimize the code needed for each endpoint. So far all of my GET requests are working. I have a POST request to add a user account working as well. The problem that I am running into is that I have a POST request to delete a user account that works if I generate the multipart/form-data (Request Body) in the API Call function but does not work if I pass the mutlipart/form-data (Request Body) in as a parameter to the API Call function.

This is Working I generate the request body with boundaries within the API call function as a string.

Function talentAPICall_3(ByVal intUserid As Integer, ByVal strPermanent As String) As String
    Dim request As New MSXML2.XMLHTTP30
    Dim apiURL, boundary, postData, strRequest, strResponse As String
    Dim contentLen As Long

    apiURL = "https://<<myDomain>>.talentlms.com/api/v1/"
    strRequest = apiURL & "deleteuser/"
    boundary = "----------------------------" & Format(Now, "ddmmyyyyhhmmss")
    postData = "--" & boundary & vbCrLf & _
        "Content-Disposition: form-data; name=""user_id""" & vbCrLf & _
        "Content-Type: text/plain; charset=UTF-8" & vbCrLf & vbCrLf & _
        intUserid & vbCrLf & _
        "--" & boundary & vbCrLf & _
        "Content-Disposition: form-data; name=""permanent""" & vbCrLf & _
        "Content-Type: text/plain; charset=UTF-8" & vbCrLf & vbCrLf & _
        strPermanent & vbCrLf & _
        "--" & boundary & "--"
    contentLen = Len(postData)

    With request
            .Open "POST", (strRequest), False
            .setRequestHeader "Authorization", "Basic <<MyAPIKey>>=="
            .setRequestHeader "Host", "<<myDomain>>.talentlms.com"
            .setRequestHeader "Content-Type", "multipart/form-data; boundary=" & boundary
            .setRequestHeader "content-Length", contentLen
            .send (postData)

            While request.ReadyState <> 4
                DoEvents
            Wend
            strResponse = .responseText         
    End With

    Debug.Print "Server responded with status " & request.statusText & " - code: "; request.status
    Debug.Print postData
    talentAPICall_3 = strResponse
End Function

This is NOT Working Where I use a getBoundaries() function to pass the body request with boundaries as a string to the API call function.

Function DelUser(ByVal intUserid As Integer, ByVal strPermanent As String) As String

Dim postData, strResponse As String
Dim boundaries() As Variant

boundaries = Array("user_id", intUserid, "permanent", strPermanent)

postData = getBoundaries(boundaries)

strResponse = talentAPICall_4(postData)

DelUser = strResponse

End Function

Which calls the following.

Function getBoundaries(params() As Variant) As String

Dim boundary, boundaries As String

boundary = "----------------------------" & Format(Now, "ddmmyyyyhhmmss")

Dim i As Long
boundaries = ""

For i = LBound(params) To UBound(params)
    boundaries = boundaries & "--" & boundary & vbCrLf & _
    "Content-Disposition: form-data; name=""" & params(i) & """" & vbCrLf & _
    "Content-Type: text/plain; charset=UTF-8" & vbCrLf & vbCrLf
    i = i + 1
    boundaries = boundaries & params(i) & vbCrLf
Next i

boundaries = boundaries & "--" & boundary & "--"

getBoundaries = boundaries

End Function

When the Request Body is generated with boundaries and returned as a string, it is then passed as a parameter to the next function.

Function talentAPICall_4(ByVal postData As String) As String

    Dim request As New MSXML2.XMLHTTP30
    Dim apiURL, boundary, strRequest, strResponse As String
    Dim contentLen As Long

    apiURL = "https://<<myDomain>>.talentlms.com/api/v1/"
    strRequest = apiURL & "deleteuser/"
    boundary = Left(postData, 44)
    contentLen = Len(postData)

    With request
            .Open "POST", (strRequest), False
            .setRequestHeader "Authorization", "Basic <<MyAPIKey>>=="
            .setRequestHeader "Host", "<<myDomain>>.talentlms.com"
            .setRequestHeader "Content-Type", "multipart/form-data; boundary=" & boundary
            .setRequestHeader "content-Length", contentLen
            .send (postData)

            While request.ReadyState <> 4
                DoEvents
            Wend
            strResponse = .responseText         
    End With

    Debug.Print "Server responded with status " & request.statusText & " - code: "; request.status
    Debug.Print postData    

    talentAPICall_4 = strResponse

End Function

Here are the results of both methods used:

Working:

call talentAPICall_3(3314, "yes")
Server responded with status OK - code:  200 

Posted Data:
------------------------------15022023131313
Content-Disposition: form-data; name="user_id"
Content-Type: text/plain; charset=UTF-8

3314
------------------------------15022023131313
Content-Disposition: form-data; name="permanent"
Content-Type: text/plain; charset=UTF-8

yes
------------------------------15022023131313--

Not Working:

call delUser(3314, "yes")
Server responded with status Bad Request - code:  400 

Posted Data:
------------------------------15022023131246
Content-Disposition: form-data; name="user_id"
Content-Type: text/plain; charset=UTF-8

3314
------------------------------15022023131246
Content-Disposition: form-data; name="permanent"
Content-Type: text/plain; charset=UTF-8

yes
------------------------------15022023131246--

As you can see, except for the variation of the time stamp used to create the boundary, the postData from Debug.Print for both functions is the same. The TalentLMS API states that the following for the 400 error code "A required parameter is missing or an invalid type (e.g. a string) was supplied instead of an integer." In both cases postData is a String and they have the same parameters.

Anyone see what I am missing?


Solution

  • In your "working" code the boundary header length is 42, and in the non-working code it's 44?

    You need to remove the leading "--"...

    boundary = Mid(postData, 3, 42)

    Might be safer to pass the boundary in to getBoundaries rather than try to extract it from the output.