如何使用 multipart/form-data 发送上传图像文件到 LINE 服务器的请求,以便将图像发布到 LINE Notify?

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

我正在尝试 post 使用 LINE Notify 从本地获取图像,但收到错误请求,如何使用 multipart/form-data 内容类型 vb.net 中的 HttpWebRequest 发送正确的请求]?

我试过 cURL,效果不错:

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"

这是我在 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
        '

        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

只发送"message"是没有问题的,每当其他部分"imageFile"、"stickerPackageId"、"stickerId"、...结合时,结果出现错误 (400)。但是,如果字段名称被更改为小写字母,即 "stickerpackageid","stickerid" vb 将不会捕获任何异常,而只是将 "message" 发送给 Notify。所以,我认为错误的部分应该在http请求字符串中,还是需要将每个字段转换为二进制数组?如果是这样如何得到正确的结果?

POST multipart/form-data 行通知,需要自己生成输出字节数组列表 LINE 通知 API 参考 https://notify-bot.line.me/doc/en/

上传文件Class

public class FormFile
{
    public string Name { get; set; }

    public string ContentType { get; set; }

    public string FilePath { get; set; }

    public byte[] bytes { get; set; }
}

测试数据

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# 代码

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;
}