Поделиться через


HttpClient and empty items in a Multipart FORM POST

It is very common to see a web page that shows a form full of data you fill out and post to web server.  Although this HTML approach works well, sometimes we want to post the same data in code instead of through a Web Browser.  We provide the HttpMultipartFormDataContent class to help you easily construct an HTTP request and mimic an HTML FORM post to a web server from client code (instead of using a web page).  However, if you wish to mimic sending a form field with no data in one or more fields, this class will fail with an exception.  This will show you how to work around these issues and give you more insight into Multipart Content customization.

The term Multipart comes from the Content-Type specified in the POST to the web server and used in the underlying HTTP protocol.  This is the RFC covering FORM data: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1

Example

To see the HTML FORM POST in action, create a simple file on your computer called form.htm.  Paste this simple html into it and save it:

Copy Code:

 <HTML>
<BODY>
 <FORM action="https://server.com/cgi/handle"
       enctype="multipart/form-data"
       method="post">
   <P>
   What is your first name? <INPUT type="text" name="first"><BR>
   What is your last name? <input type="text" name="last"><br>
   <INPUT type="submit" value="Send"> <INPUT type="reset">
 </FORM>
</BODY></HTML>

Next, download a tool to inspect the HTML (I used Fiddler) and run it, open the form.htm file in Internet Explorer, fill in your first and last name, hit the Send button and look at the traffic generated for the POST (Note: server.com will not resolve but you will be able to see what was transmitted anyhow):

POST https://server.com/cgi/handle HTTP/1.1

Accept: text/html, application/xhtml+xml, */*

Accept-Language: en-US

Content-Type: multipart/form-data; boundary= ---------------------------7de1081a1504ac

User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko

Accept-Encoding: gzip, deflate

Host: server.com

Content-Length: 247

DNT: 1

Connection: Keep-Alive

Pragma: no-cache

-----------------------------7de1081a1504ac
Content-Disposition: form-data; name="first"

Jeff

-----------------------------7de1081a1504ac
Content-Disposition: form-data; name="last"

Sanders

-----------------------------7de1081a1504ac--

 

Now if I don’t fill in the last name, here is what I see:

POST https://server.com/cgi/handle HTTP/1.1

Accept: text/html, application/xhtml+xml, */*

Accept-Language: en-US

Content-Type: multipart/form-data; boundary=
---------------------------7de1a321504ac
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko

Accept-Encoding: gzip, deflate

Host: server.com

Content-Length: 237

DNT: 1

Connection: Keep-Alive

Pragma: no-cache

-----------------------------7de1a321504ac

Content-Disposition: form-data; name="first"

Jeff

-----------------------------7de1a321504ac
Content-Disposition: form-data; name="last"

-----------------------------7de1a321504ac--

Note that the last form field has no data!

What you need to know about the multipart/form information above:

  • Content-Type tells the server that it is multipart/form so expect the data (form fields) to be separated by the boundary specified (in this case: ---------------------------7de1a321504ac)
  • Each form field has a name, followed a blank line and the actual data (or lack of data).

 

How to duplicate this without using a WebBrowser control in C#

This code is used to show the issue and build various solutions.  Create a blank C#/XAML Windows Store app and replace the code inside your MainPage class with this.

You will need to add these using statements as well:

using System.Threading.Tasks;

using Windows.Web.Http;

Copy Code:

 public MainPage()
        {
            this.InitializeComponent();
            m_HttpClient = new HttpClient();
        }
        private HttpClient m_HttpClient;
        private static Uri resourceAddress = new Uri("https://server.com/cgi/handle");
        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            await PostFormData();
        }

        
        // Post using MultiPart class only.
        // If you attempt to add a field that is an empty string 
        // you get an error: 
        //
        private async Task PostFormData()
        {
            try
            {
                // create the form helper
                HttpMultipartFormDataContent form = new HttpMultipartFormDataContent();

                // add data from text boxes on form
                form.Add(new HttpStringContent(txtFirst.Text), "first");
                form.Add(new HttpStringContent(txtLast.Text), "last");

                // Create a request
                HttpRequestMessage aReq = new HttpRequestMessage(HttpMethod.Post, resourceAddress);
                // set the form data on it
                aReq.Content = form;

                // get the response
                HttpResponseMessage response = await m_HttpClient.SendRequestAsync(aReq);
                txtResult.Text = await response.Content.ReadAsStringAsync();
            }
            catch (Exception ex)
            {
                txtResult.Text = "ERROR: " + ex.Message;
            }
        }

XAML for above:

Copy Code:

 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox x:Name="txtFirst" HorizontalAlignment="Left" Margin="55,50,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" RenderTransformOrigin="-0.095,-4.073" Width="231"/>
        <TextBox x:Name="txtLast" HorizontalAlignment="Left" Margin="55,119,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="231"/>
        <TextBox x:Name="txtResult" HorizontalAlignment="Left" Margin="55,279,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Height="336" Width="231"/>
        <Button Content="Submit" HorizontalAlignment="Left" Margin="209,181,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.003,0.118" Click="Button_Click"/>
</Grid>

This first code sample will POST two FORM fields to the server successfully if there is data for both of the fields entered.  You can see it is very similar to the data POSTed from Internet Explorer:

POST https://server.com/cgi/handle HTTP/1.1

Accept-Encoding: gzip, deflate

Content-Length: 352

Content-Type: multipart/form-data; boundary=a1161a53-ebaf-4d53-baef-315de3e2b67f

Host: server.com

Connection: Keep-Alive

Pragma: no-cache

--a1161a53-ebaf-4d53-baef-315de3e2b67f

Content-Length: 4

Content-Type: text/plain; charset=UTF-8

Content-Disposition: form-data; name="first"

Jeff

--a1161a53-ebaf-4d53-baef-315de3e2b67f

Content-Length: 7

Content-Type: text/plain; charset=UTF-8

Content-Disposition: form-data; name="last"

Sanders

--a1161a53-ebaf-4d53-baef-315de3e2b67f--

The Problem

If data is missing from one or both of the fields you will get this error:

Error:

Value does not fall within the expected range.

Cause:

Typically you are passing an empty string into the HttpStringContent class when constructing FORM data with the HttpMultipartFormDataContent class.

 

You can argue that you should not be posting anything that does not have a value, however there may be cases (a server you do not have control over for example) that you need to send FORM data with empty fields.

Solutions

Test for and don’t pass empty data fields.

This function is very similar to the first, however if a field is missing data, we simply do not include the HttpStringContent for it:

Copy Code:

         // Same as 'PostFormData' except only transmit 
        // form fields that are not empty strings
        private async Task PostFormDataIfProvided()
        {
            try
            {
                // create the form helper
                HttpMultipartFormDataContent form = new HttpMultipartFormDataContent();

                // add data from text boxes on form ONLY
                // if there is data entered in the text box
                if (txtFirst.Text.Length != 0)
                {
                    form.Add(new HttpStringContent(txtFirst.Text), "first");
                }

                if (txtLast.Text.Length != 0)
                {
                    form.Add(new HttpStringContent(txtLast.Text), "last");
                }

                // Create a request
                HttpRequestMessage aReq = new HttpRequestMessage(HttpMethod.Post, resourceAddress);
                // set the form data on it
                aReq.Content = form;

                // get the response
                HttpResponseMessage response = await m_HttpClient.SendRequestAsync(aReq);
                txtResult.Text = await response.Content.ReadAsStringAsync();
            }
            catch (Exception ex)
            {
                txtResult.Text = "ERROR: " + ex.Message;
            }
        }

Manually Generate the Multipart data

You gain a lot of control over the Multipart data and how it is presented by manually generating the boundary and data contained in the POST.  It IS just HTTP data after all!   This sample shows generating Text fields to send and will send empty fields as well since we are not testing for this.

Copy Code:

         // Manually generate the Multipart data (not very difficult actually and gives you more control).
        private async Task PostFormDataIfEmpty()
        {
            try
            {
                // the Multipart boundary is a unique key that you do not expect to see in your text data..
                String strMultipartBoundary = "MyMadeUpBoundary-2320-332-567534";

                //Create a string of the data you wish to POST
                StringBuilder form = new StringBuilder();
                // ensure each form field begins with the boundary, is the correct format and has the basic information we need.
                // this will transmit empty strings for the form as well
                form.Append(String.Format("--{0} \r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n", strMultipartBoundary, "first", txtFirst.Text));
                form.Append(String.Format("--{0} \r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n", strMultipartBoundary, "last", txtLast.Text));
                form.Append(String.Format("--{0}--\r\n",strMultipartBoundary));

                // Create a request
                HttpRequestMessage aReq = new HttpRequestMessage(HttpMethod.Post, resourceAddress);
                // Manually set the content type and the form data as HttpStringContent
                aReq.Content = new HttpStringContent(form.ToString(), Windows.Storage.Streams.UnicodeEncoding.Utf8, "multipart/form-data; boundary=" + strMultipartBoundary  );

                // get the response
                HttpResponseMessage response = await m_HttpClient.SendRequestAsync(aReq);
                txtResult.Text = await response.Content.ReadAsStringAsync();
            }
            catch (Exception ex)
            {
                txtResult.Text = "ERROR: " + ex.Message;
            }

        }

Hybrid Solution using manual and automatic generation methods

You may find this useful to leverage some of the built in classes and combine this with manual generation.  This has a little more code but may be useful if you are utilizing other HttpContent derived classes.

Copy Code:

         // Combines using the MultiPartForm classes and manually generating the multipart info
        private async Task PostFormDataIfEmptyHybrid()
        {
            try
            {
                // We will get the boundary string generated by the class below so it is the same for autogenerated
                // and manually generated multipart info
                String strMultipartBoundary = "";

                // used to generate form info
                HttpMultipartFormDataContent form = new HttpMultipartFormDataContent();
                // if needed, to store manually generate multipart info (for instance field with no data set)
                StringBuilder buildform = new StringBuilder();

                foreach (var param in form.Headers.ContentType.Parameters.Where(param => param.Name.Equals("boundary")))
                {
                    // get the multipart boundary field generated by this class (guid like)
                    strMultipartBoundary = param.Value;
                }

                // if text was entered, use the Multipart form class
                if (txtFirst.Text.Length != 0)
                {
                    form.Add(new HttpStringContent(txtFirst.Text), "first");
                }
                else // otherwise we will have some manually generated multipart information
                {
                    buildform.Append(String.Format("--{0} \r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n", strMultipartBoundary, "first", txtFirst.Text));
                }

                if (txtLast.Text.Length != 0)
                {
                    form.Add(new HttpStringContent(txtLast.Text), "last");
                }
                else
                {
                    buildform.Append(String.Format("--{0} \r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n", strMultipartBoundary, "last", txtLast.Text));
                }


                HttpRequestMessage aReq = new HttpRequestMessage(HttpMethod.Post, resourceAddress);

                if (buildform.Length>0) //if we have manually generated data...
                {
                    if (form.Count() !=0 ) // if there is automatically generated data too...
                    {
                        // get the string representation of it (included the closing boundary tag)
                        string formStr = await form.ReadAsStringAsync();
                        // append it
                        buildform.Append(formStr);
                    }
                    else
                    {
                        // close the multipart boundary section.
                        buildform.Append(String.Format("--{0}--\r\n", strMultipartBoundary));
                    }

                    // set the content on the request
                    aReq.Content = new HttpStringContent(buildform.ToString(), Windows.Storage.Streams.UnicodeEncoding.Utf8, "multipart/form-data; boundary=" + strMultipartBoundary);
                }
                else // otherwise no manually generated data
                {
                    // just set the form data on it
                    aReq.Content = form;
                }

                // get the response
                HttpResponseMessage response = await m_HttpClient.SendRequestAsync(aReq);
                txtResult.Text = await response.Content.ReadAsStringAsync();
            }
            catch (Exception ex)
            {
                txtResult.Text = "ERROR: " + ex.Message;
            }
        }

Customization of Multipart Content

It is not a huge leap for you to see how you can utilize code similar to this to produce any Multipart and Multipart Form content you may need using HttpClient.  You simply can use the Hybrid example or manually generate your content and do that fairly easily.

Troubleshooting

Use Fiddler to troubleshoot.  For example, I used Fiddler to capture a standard HTML form POST of form data and compared it to what the classes mentioned above were doing.  There are lots of resources like this and you can use https://bing.com for more hints.  Also become familiar with https://w3c.org and you will be able to look up what should be transmitted over the web!

Here is another error you may see while playing with the HttpMultipartFormDataContent class

Error:

A method was called at an unexpected time.

HttpMultipartContent requires there to be at least one part.

Cause:

You have not added any HttpContent to the HttpMultipartFormDataContent class.

 

Summary

This should get you started with your Multipart programming! 

Follow me @jsandersrocks and my team at @WSDevSol on Twitter.

Be sure to drop me a note if you find this useful!

Fiddler

https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2

https://www.ietf.org/rfc/rfc2388.txt

Comments

  • Anonymous
    March 29, 2015
    Thanks buddy for saring, I did the same but there is only one thing I can't change based on what am getting from fiddler, If i use a normal web form, the request initiated with content type text/xml and when i do this through the code its initiated as text/html which is making a prolbem with the API and calling, can you please recommend what need to be done !!!

  • Anonymous
    February 04, 2016
    Thank you!! it is very helpful for me!