POST для многочастной формы для Bamboo API

У меня возникли трудности с отправкой многочастной формы через консольное приложение VB.NET в BambooHR API. Я отправил свой текущий код, а также образец запроса из приведенной ниже документации, когда я запускаю этот код, который я получаю (400) Bad Request. Я знаю, что код грязный, но я просто пытался заставить его работать.

Я смог выполнить работу с запросом GET, используя их пример кода, но у них не было никакого кода для выполнения этого конкретного вызова API (загрузка файла сотрудника).

ЛЮБАЯ помощь будет оценена.

Вот мой код:

Sub Main()

    upload(id, "https://api.bamboohr.com/api/gateway.php/company")

    Console.WriteLine()
    Console.WriteLine("Press ENTER to quit")
    Console.ReadLine()
End Sub

Function upload(ByVal employeeId As Integer, ByVal baseUrl As String)

    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 Or SecurityProtocolType.Ssl3

    Dim boundary = "----BambooHR-MultiPart-Mime-Boundary----"
    Dim url = String.Format("{0}/v1/employees/{1}/files/", baseUrl, employeeId)

    Dim request As HttpWebRequest = WebRequest.Create(url)
    request.KeepAlive = True
    request.Method = "POST"
    request.ContentType = "multipart/form-data; boundary=" + boundary

    'Authorization is just the api key and a random string, in this case is x
    '
    Dim authInfo As String = api_key + ":" + "x"
    authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo))
    request.Headers("Authorization") = "Basic " + authInfo


    Dim memStream As New MemoryStream()

    WriteMPF(memStream)

    request.ContentLength = memStream.Length

    Using requestStream = request.GetRequestStream()
        memStream.Position = 0
        Dim tempBuffer As Byte() = New Byte(memStream.Length - 1) {}
        memStream.Read(tempBuffer, 0, tempBuffer.Length)
        memStream.Close()
        requestStream.Write(tempBuffer, 0, tempBuffer.Length)
    End Using

    Dim webresponse As HttpWebResponse = request.GetResponse()
    Return webresponse

End Function

Private Sub WriteMPF(s As Stream)

    WriteToStream(s, "POST /api/gateway.php/company/v1/employees/id/files/ HTTP/1.0")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Host: api.bamboohr.com")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Content-Type: multipart/form-data; boundary=----BambooHR-MultiPart-Mime-Boundary----")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Content-Length: 520")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, vbCr & vbLf)

    WriteToStream(s, "------BambooHR-MultiPart-Mime-Boundary----")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Content-Disposition: form-data; name=""category""")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "14")
    WriteToStream(s, vbCr & vbLf)

    WriteToStream(s, "------BambooHR-MultiPart-Mime-Boundary----")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Content-Disposition: form-data; name=""fileName""")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "test.txt")
    WriteToStream(s, vbCr & vbLf)

    WriteToStream(s, "------BambooHR-MultiPart-Mime-Boundary----")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Content-Disposition: form-data; name=""share""")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "no")
    WriteToStream(s, vbCr & vbLf)

    WriteToStream(s, "------BambooHR-MultiPart-Mime-Boundary----")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Content-Disposition: form-data; name=""file""; filename = ""test.txt""")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "Content-Type: text/plain")
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "this is a test!")
    WriteToStream(s, vbCr & vbLf)

    WriteToStream(s, vbCr & vbLf)
    WriteToStream(s, "------BambooHR-MultiPart-Mime-Boundary------")
    WriteToStream(s, vbCr & vbLf)
End Sub

Private Sub WriteToStream(s As Stream, txt As String)
    Dim bytes As Byte() = Encoding.UTF8.GetBytes(txt)
    s.Write(bytes, 0, bytes.Length)
End Sub

Вот пример запроса из документации: (ссылка: https://www.bamboohr.com/api/documentation/employees.php прокрутите вниз до "Загрузить файл сотрудника" )

POST/api/gateway.php/sample/v1/employees/1/files/HTTP/1.0 Хост: api.bamboohr.com Content-Type: multipart/form-data; граница = ---- BambooHR-MULTIPART-мим-Boundary ---- Content-Length: 520

------ BambooHR-MULTIPART-мим-Boundary ---- Content-Disposition: form-data; name= "категория"

112 ------ BambooHR-MULTIPART-мим-Boundary ---- Content-Disposition: form-data; name= "имя_файла"

readme.txt ------ BambooHR-MULTIPART-мим-Boundary ---- Content-Disposition: form-data; name= "доля"

да ------ BambooHR-MULTIPART-мим-Boundary ---- Content-Disposition: form-data; name= "файл"; имя файла = "readme.txt" Content-Type: text/plain

Это пример текстового файла.

------ BambooHR-MULTIPART-мим-Граница ------

Ответ 1

Использовал пример php на своем GitHub и скопировал его на VB.NET. Это немного грязно, но это работает. Вот соответствующий код:

Public Function sendRequestMPF(ByVal req As BambooHTTPRequest, ByVal fileLocation As String) As BambooHTTPResponse
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 Or SecurityProtocolType.Ssl3

        Dim request As HttpWebRequest = WebRequest.Create(req.url)
        request.Method = req.method
        request.Host = "api.bamboohr.com"

        Dim boundary = "----BambooHR-MultiPart-Mime-Boundary----"

        Try
            request.ContentType = "multipart/form-data; boundary=" + boundary
            request.ContentLength = req.contents.Length
        Catch ex As Exception

        End Try

        Dim iCount As Integer = req.headers.Count
        Dim key As String
        Dim keyvalue As String

        Dim i As Integer
        For i = 0 To iCount - 1
            key = req.headers.Keys(i)
            keyvalue = req.headers(i)
            request.Headers.Add(key, keyvalue)
        Next

        Dim enc As System.Text.UTF8Encoding = New System.Text.UTF8Encoding()
        Dim bytes() As Byte = {}
        Dim pdfBytes() As Byte = {}
        Dim lBytes() As Byte = {}

        Dim fBytes() As Byte = {}
        Dim s As New MemoryStream()

        If (req.contents.Length > 0) Then
            bytes = enc.GetBytes(req.contents)
            s.Write(bytes, 0, bytes.Length)

            pdfBytes = File.ReadAllBytes(fileLocation)
            s.Write(pdfBytes, 0, pdfBytes.Length)

            Dim postHeader = vbCrLf + vbCrLf + "--" + boundary + "--" + vbCrLf
            Dim postHeaderBytes() As Byte = enc.GetBytes(postHeader)
            lBytes = enc.GetBytes(postHeader)
            s.Write(postHeaderBytes, 0, postHeaderBytes.Length)

            fBytes = s.ToArray()
            request.ContentLength = fBytes.Length
        End If

        request.AllowAutoRedirect = False

        If Not basicAuthUsername.Equals("") Then
            Dim authInfo As String = basicAuthUsername + ":" + basicAuthPassword
            authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo))
            request.Headers("Authorization") = "Basic " + authInfo
        End If

        If req.contents.Length > 0 Then
            Dim outBound As Stream = request.GetRequestStream()
            outBound.Write(fBytes, 0, fBytes.Length)
        End If

        Dim resp As BambooHTTPResponse
        Try
            Dim webresponse As HttpWebResponse = request.GetResponse()
            resp = New BambooHTTPResponse(webresponse)
            resp.responseCode = webresponse.StatusCode
            resp.headers = webresponse.Headers
        Catch e As WebException
            Console.WriteLine(e.Message)
            If (e.Status = WebExceptionStatus.ProtocolError) Then
                resp = New BambooHTTPResponse(DirectCast(e.Response, HttpWebResponse).StatusCode)
            Else
                resp = New BambooHTTPResponse(0)
            End If
        End Try

        Return resp
    End Function

Public Function buildMultiPart(ByVal params As NameValueCollection, ByVal boundary As String, ByVal contentType As String, ByVal name As String, ByVal fileName As String)
        Dim data = ""

        For Each key In params.AllKeys
            data += "--" + boundary + vbCrLf
            data += "Content-Disposition: form-data; name=""" + key + """"
            data += vbCrLf + vbCrLf
            data += params(key) + vbCrLf
        Next

        data += "--" + boundary + vbCr + vbLf
        data += "Content-Disposition: form-data; name=""" + name + """;" + " filename=""" + fileName + """" + vbCrLf
        data += "Content-Type: " + contentType + vbCrLf
        data += vbCrLf
        'data += fileData + vbCrLf + vbCrLf
        'data += "--" + boundary + "--" + vbCrLf

        Return data
    End Function

    Public Function uploadEmployeeFile(ByVal employeeId As Integer, ByVal fileName As String, ByVal fileLocation As String)
        Dim request As New BambooHTTPRequest()
        request.url = String.Format("{0}/v1/employees/{1}/files/", Me.baseUrl, employeeId)
        request.method = "POST"

        Dim boundary = "----BambooHR-MultiPart-Mime-Boundary----"

        Dim params = New NameValueCollection
        params.Add("category", "13")
        params.Add("fileName", fileName)
        params.Add("share", "no")

        request.contents = buildMultiPart(params, boundary, "application/pdf", "file", fileName)

        Return http.sendRequestMPF(request, fileLocation)
    End Function

Остальная часть необходимого кода может быть найдена на их GitHub https://github.com/BambooHR

Ответ 2

Я подозреваю, что по крайней мере ваш Content-Length: 520 будет неправильным. Эта длина содержимого применима только к их примеру.

В любом случае, я не писал VB.Net в течение долгого времени, но из быстрого теста модифицированная версия этого кода работает против одной из моих служб REST, поэтому она должна работать в вашем случае, возможно, с некоторыми незначительная настройка.

Мой проект тестовой консоли использовал .Net 4.6.1, но, скорее всего, будет работать с некоторыми из ранних .Net Framework.

Imports System.IO
Imports System.Net.Http

Module Module1

    Sub Main()
        Call UploadFileToWebsite(14, "no", "D:\Temp\file.pdf")
        Console.WriteLine("Please wait for a response from the server and then press a key to continue.")
        Console.ReadKey()
    End Sub

    Public Sub UploadFileToWebsite(category As Integer, share As String, file As String)
        Dim message = New HttpRequestMessage()
        Dim content = New MultipartFormDataContent()

        content.Add(New StringContent(category.ToString()), "category")
        content.Add(New StringContent(share), "share")

        Dim filestream = New FileStream(file, FileMode.Open)
        Dim fileName = System.IO.Path.GetFileName(file)

        content.Add(New StreamContent(filestream), "file", fileName)

        message.Method = HttpMethod.Post
        message.Content = content
        message.RequestUri = New Uri("https://api.bamboohr.com/api/gateway.php/company")

        Dim client = New HttpClient()
        client.SendAsync(message).ContinueWith(
            Sub(task)
                'do something with response
                If task.Result.IsSuccessStatusCode Then
                    Console.WriteLine("Uploaded OK.")
                Else
                    Console.WriteLine("Upload Failed.")
                End If
            End Sub)
    End Sub
End Module

В несвязанной заметке вы также можете использовать vbCrLf вместо vbCr & vbLf.