Search code examples
vb.netmultipartform-dataline-api

How to send a request to upload image file to LINE server with multipart/form-data for posting image to LINE Notify?


I'm trying to post an image from local using LINE Notify, but get bad request, how to send the right request using HttpWebRequest with multipart/form-data content-type in vb.net?

I've tried on cURL, and it's woking:

curl -i -X POST https://notify-api.line.me/api/notify -H "Authorization: Bearer <TOKEN>" -F "message=test" -F "imageFile=@C:\PATH\to\file.jpg"

Here is what I've done in vb.net:

Imports System.IO
Imports System.Net
Imports System.Security.Cryptography
Imports System.Text

Public Class Form1
    Private Sub btnPic_Click(sender As Object, e As EventArgs) Handles btnPic.Click
        OpenFileDialog1.Filter = "Image File (*.jpg)|*.jpg;*.JPG |Image File (*.png)|*.png;*.PNG"
        OpenFileDialog1.FileName = ""

        If OpenFileDialog1.ShowDialog() <> DialogResult.Cancel Then
            txtPic.Text = OpenFileDialog1.FileName
        End If
    End Sub

    Private Sub btnSend_Click(sender As Object, e As EventArgs) Handles btnSend.Click
        ' [references]
        'https://notify-bot.line.me/doc/en/
        'http://white5168.blogspot.com/2017/01/line-notify-6-line-notify.html
        'https://aprico-media.com/posts/1824
        'https://stackoverflow.com/questions/3890754/using-httpwebrequest-to-post-data-upload-image-using-multipart-form-data

        Dim md5Hash As MD5 = MD5.Create()
        'Boundary string
        Dim strBound As String = "---------------------" & GetMd5Hash(md5Hash, CInt(Int((654321 * Rnd()) + 1))).Substring(0, 10)

        Try
            Dim request = DirectCast(WebRequest.Create("https://notify-api.line.me/api/notify"), HttpWebRequest)
            Dim postData As String = "--" & strBound & vbCrLf
            postData += "Content-Disposition: form-data; name=""message""" & vbCrLf & vbCrLf
            postData += txtText.Text & vbCrLf
            ' [Not working] sticker part
            'postData += "--" & strBound & vbCrLf
            'postData += "Content-Disposition: form-data; name=""stickerPackageId""" & vbCrLf & vbCrLf
            'postData += 1 & vbCrLf
            'postData += "--" & strBound & vbCrLf
            'postData += "Content-Disposition: form-data; name=""stickerId""" & vbCrLf & vbCrLf
            'postData += 2 & vbCrLf

            If txtPic.Text <> "" Then
                Dim ext As String = Path.GetExtension(txtPic.Text)
                If ext = ".jpg" Then
                    ext = "jpeg"
                ElseIf ext = ".png" Then
                    ext = "png"
                Else
                    MessageBox.Show("Sorry! LINE Notify supports only jpeg/png image file.")
                    btnPic.PerformClick()
                    Return
                End If
                ' [Not working] image part
                postData += "--" & strBound & vbCrLf
                postData += "Content-Disposition: form-data; name=""imageFile""; filename=""" & Path.GetFileName(txtPic.Text) & """" & vbCrLf
                postData += "Content-Type: image/" & ext & vbCrLf & vbCrLf
                postData += Convert.ToBase64String(File.ReadAllBytes(txtPic.Text)) & vbCrLf
            End If
            postData += vbCrLf & "--" & strBound & "--"
            Dim data = Encoding.UTF8.GetBytes(postData)

            request.Method = "POST"
            request.ContentType = "multipart/form-data; boundary=" & strBound
            request.ContentLength = data.Length
            request.Headers.Add("Authorization", "Bearer <TOKEN>")

            Using stream = request.GetRequestStream()
                stream.Write(data, 0, data.Length)
            End Using

            Dim response = DirectCast(request.GetResponse(), HttpWebResponse)
            Dim responseString = New StreamReader(response.GetResponseStream()).ReadToEnd()
        Catch ex As Exception
            MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1)
        End Try
    End Sub

    Shared Function GetMd5Hash(ByVal md5Hash As MD5, ByVal Input As String) As String

        ' Convert the input string to a byte array and compute the hash.
        Dim data As Byte() = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(Input))

        ' Create a new Stringbuilder to collect the bytes
        ' and create a string.
        Dim sBuilder As New StringBuilder()

        ' Loop through each byte of the hashed data 
        ' and format each one as a hexadecimal string.
        Dim i As Integer
        For i = 0 To data.Length - 1
            sBuilder.Append(data(i).ToString("x2"))
        Next i

        ' Return the hexadecimal string.
        Return sBuilder.ToString()

    End Function 'GetMd5Hash

End Class

There is no problem for sending only "message", whenever the other parts i.e."imageFile", "stickerPackageId", "stickerId",... are combined with, the result is getting error (400). However, if a field name is changed to small letter as i.e."stickerpackageid", "stickerid" vb will not catch any exception, but just send the "message" to Notify. So, I think a wrong part should be in http request string, or does it needed to convert each field into binary array? If that is the case how to obtain the right result?


Solution

  • POST multipart/form-data for line notify , need generate output byte array list by yourself LINE Notify API Reference https://notify-bot.line.me/doc/en/

    Upload File Class

    public class FormFile
    {
        public string Name { get; set; }
    
        public string ContentType { get; set; }
    
        public string FilePath { get; set; }
    
        public byte[] bytes { get; set; }
    }
    

    Test Data

    private void multipartTest(string access_token)
    {
        Dictionary<string, object> d = new Dictionary<string, object>()
        {
            // message , imageFile ... name is provided by LINE API
            { "message", @"message..." },
            { "imageFile", new FormFile(){ Name = "notify.jpg", ContentType = "image/jpeg", FilePath="notify.jpg" }
            }
        };
    
        string boundary = "Boundary";
        List<byte[]> output = genMultPart(d, boundary);
        lineNotifyMultipart(access_token, boundary, output);
    }
    

    C# Code

    private void lineNotifyMultipart(string access_token, string boundary, List<byte[]> output)
    {
        try
        {
            #region POST multipart/form-data
            StringBuilder sb = new StringBuilder();
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(lineNotifyURL);
            request.Method = "POST";
            request.ContentType = "multipart/form-data; boundary=Boundary";
            request.Timeout = 30000;
    
            // header
            sb.Clear();
            sb.Append("Bearer ");
            sb.Append(access_token);
            request.Headers.Add("Authorization", sb.ToString());
            // note: multipart/form-data boundary must exist in headers ContentType
            sb.Clear();
            sb.Append("multipart/form-data; boundary=");
            sb.Append(boundary);
            request.ContentType = sb.ToString();
    
            // write Post Body Message
            BinaryWriter bw = new BinaryWriter(request.GetRequestStream());
            foreach(byte[] bytes in output)
                bw.Write(bytes);
    
            #endregion
    
            getResponse(request);
        }
        catch (Exception ex)
        {
            #region Exception
            StringBuilder sbEx = new StringBuilder();
            sbEx.Append(ex.GetType());
            sbEx.AppendLine();
            sbEx.AppendLine(ex.Message);
            sbEx.AppendLine(ex.StackTrace);
            if (ex.InnerException != null)
                sbEx.AppendLine(ex.InnerException.Message);
            myException ex2 = new myException(sbEx.ToString());
            //message(ex2.Message);
            #endregion
        }
    }
    
    private void getResponse(HttpWebRequest request)
    {
        StringBuilder sb = new StringBuilder();
        string result = string.Empty;
        StreamReader sr = null;
        try
        {
            #region Get Response
            if (request == null)
                return;
            // HttpWebRequest GetResponse() if error happened will trigger WebException
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
            {
                sb.AppendLine();
                foreach (var x in response.Headers)
                {
                    sb.Append(x);
                    sb.Append(" : ");
                    sb.Append(response.Headers[x.ToString()]);
                    if (x.ToString() == "X-RateLimit-Reset")
                    {
                        sb.Append(" ( ");
                        sb.Append(CheckFormat.ToEpcohDateTimeUTC(long.Parse(response.Headers[x.ToString()])));
                        sb.Append(" )");
                    }
                    sb.AppendLine();
                }
                using (sr = new StreamReader(response.GetResponseStream()))
                {
                    result = sr.ReadToEnd();
                    sb.Append(result);
                }
            }
    
            //message(sb.ToString());
    
            #endregion
        }
        catch (WebException ex)
        {
            #region WebException handle
            // WebException Response
            using (HttpWebResponse response = (HttpWebResponse)ex.Response)
            {
                sb.AppendLine("Error");
                foreach (var x in response.Headers)
                {
                    sb.Append(x);
                    sb.Append(" : ");
                    sb.Append(response.Headers[x.ToString()]);                    
                    sb.AppendLine();
                }
                using (sr = new StreamReader(response.GetResponseStream()))
                {
                    result = sr.ReadToEnd();
                    sb.Append(result);
                }
    
                //message(sb.ToString());
            }
    
            #endregion
        }
    }
    
    public List<byte[]> genMultPart(Dictionary<string, object> parameters, string boundary)
    {
        StringBuilder sb = new StringBuilder();
        sb.Clear();
        sb.Append("\r\n--");
        sb.Append(boundary);
        sb.Append("\r\n");
        string beginBoundary = sb.ToString();
        sb.Clear();
        sb.Append("\r\n--");
        sb.Append(boundary);
        sb.Append("--\r\n");
        string endBoundary = sb.ToString();
        sb.Clear();
        sb.Append("Content-Type: multipart/form-data; boundary=");
        sb.Append(boundary);
        sb.Append("\r\n");
        List<byte[]> byteList = new List<byte[]>();
        byteList.Add(System.Text.Encoding.UTF8.GetBytes(sb.ToString()));
    
        foreach (KeyValuePair<string, object> pair in parameters)
        {
            if (pair.Value is FormFile)
            {
                byteList.Add(System.Text.Encoding.ASCII.GetBytes(beginBoundary));
                FormFile form = pair.Value as FormFile;
    
                sb.Clear();
                sb.Append("Content-Disposition: form-data; name=\"");
                sb.Append(pair.Key);
                sb.Append("\"; filename=\"");
                sb.Append(form.Name);
                sb.Append("\"\r\nContent-Type: ");
                sb.Append(form.ContentType);
                sb.Append("\r\n\r\n");
                byte[] bytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
                byteList.Add(bytes);
                if (form.bytes == null && !string.IsNullOrEmpty(form.FilePath))
                {    
                    FileStream fs = new FileStream(form.FilePath, FileMode.Open, FileAccess.Read);
                    MemoryStream ms = new MemoryStream();
                    fs.CopyTo(ms);
                    byteList.Add(ms.ToArray());
                }
                else
                    byteList.Add(form.bytes);
            }
            else
            {
                byteList.Add(System.Text.Encoding.ASCII.GetBytes(beginBoundary));
                sb.Clear();
                sb.Append("Content-Disposition: form-data; name=\"");
                sb.Append(pair.Key);
                sb.Append("\"");
                sb.Append("\r\n\r\n");
                sb.Append(pair.Value);
                string data = sb.ToString();
                byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data);
                byteList.Add(bytes);
            }
        }
    
        byteList.Add(System.Text.Encoding.ASCII.GetBytes(endBoundary));
        return byteList;
    }