June 11, 2014

multipart/form-data: HTML file upload internal explained with C# implementation


In this post I will explain how browser encode an html form before sending to server and how to use a C# program to perform same type of encoding.

Below is a very basic html form with a file and two text html controls.

<!DOCTYPE html>
<html>
  <body>
    <form action="http://localhost:80/User/Register.php" method="post" 
          enctype="multipart/form-data">
      First name: <input type="text" name="fname"><br>
      Last name:  <input type="text" name="lname"><br>
      Photo:      <input type="file" name="photo"><br>
      <input type="submit" value="Submit">
    </form>
  </body>
</html>

This will render the form like:



When we click submit, browser encode the form data and send it to server. 


Note that we declare "multipart/form-data" (via form enctype attribute) as encoding to be used, this is the form encoding
that supports forms with 'file' control. Another encoding type (default) is "application/x-www-form-urlencoded"


If we look into the request send to the server using fiddler, it looks like:





The Content-Type "multipart/form-data" specifies how the form data set is encoded. Form data set is the sequence
of "name and value" of html controls defined inside 'form' node.

For example in the above form, data set is { { fname, "anu" }, { lname "chandy" }, { photo "<content-of-file>" } }

The encoding "multipart/form-data" says,  message contains a series of parts each part contain a name-value pair in
the 'form data set'. These parts are in the same order as the corresponding controls in the form. 

Value of the boundary attribute (in the above example "----WebKitFormBoundaryUOaE2JpZxwqbIieW") declare the boundary
that separates each parts.



Each part has following format:


// New line
// New line
--<value-of-boundary-attribute>
Content-Disposition: form-data; name="<control-name>" [filename="name-of-the-file-if-control-is-file"]
[Content-Type: <The-content-type-of-uploded-file-if-control-is-file>]
// New line
// New line
Value of the control or file content if control is 'file'


Each part starts with two dashes (--) followed by the boundary string defined by the Content-Type boundary attribute. 

After all parts, the message ends with a string that is formed by appending and prep-ending the boundary attribute by two dashed (--) 






Below is a C# program that shows how to use MultipartFormDataContent class to generate a message that conform to "multipart/form-data" protocol.

The usage is:

NameValueCollection formdata = new NameValueCollection();
myCol.Add( "fname", "anu" );
myCol.Add( "lname", "chandy" );

SubmitFormViaPOST("http://localhost:80/User/Register.php", // Server url
            "photo", // Name for file control
            "./images/MyProfilePic.jpg", // Path to the file to be uploaded
            "image/jpg" // The file Content Type,
            formdata, // Other form data
            CancellationToken.None).Wait();

Function definition is:


public async Task SubmitFormViaPOST(string serverUrl,
            string fileControlName,
            string filePath,
            string fileContentType,
            NameValueCollection formData,
            CancellationToken cancellationToken)
        {
            FileInfo fileInfo = new FileInfo(filePath);
            Uri RequestUri = new Uri(serverUrl);

            using (var multiPartContent = new MultipartFormDataContent("---------------------------" + DateTime.Now.Ticks.ToString("x")))
            {
                #region Build Request Content

                foreach (string key in formData)
                {
                    multiPartContent.Add(new StringContent(formData[key]), key);
                }

                FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
                StreamContent streamContent = new StreamContent(fileStream);
                multiPartContent.Add(streamContent, fileControlName, filePath);
                streamContent.Headers.ContentType = new MediaTypeHeaderValue(fileContentType);

                #endregion

                #region creates HttpRequestMessage object

                HttpRequestMessage httpRequest = new HttpRequestMessage();
                httpRequest.Method = HttpMethod.Post;
                httpRequest.RequestUri = RequestUri;
                httpRequest.Content = multiPartContent;

                #endregion

                #region Send the request and process response
                // Send Request

                HttpResponseMessage httpResponse = null;
                try
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    HttpClient httpClient = new HttpClient();
                    httpClient.Timeout = TimeSpan.FromSeconds(300);
                    httpResponse = await httpClient.
                        SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);

                    HttpStatusCode statusCode = httpResponse.StatusCode;
                    if (statusCode != HttpStatusCode.OK)
                    {
                        string errorResponse =
                            await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

                        throw new Exception(errorResponse);
                    }
                }
                finally
                {
                    if (httpResponse != null)
                    {
                        httpResponse.Dispose();
                    }
                }

                #endregion
            }
        }


The above function uses MultipartFormDataContent which abstract the logic associated with generating the message.  Take a look at below function which provide exactly same functionality as above but it takes care of the raw message generation that conform to "multipart/form-data" protocol.




public async Task SubmitFormViaPOST(string serverUrl,
            string fileControlName,
            string filePath, 
            string fileContentType, 
            NameValueCollection formData, 
            CancellationToken cancellationToken)
        {
            FileInfo fileInfo = new FileInfo(filePath);
            Uri RequestUri = new Uri(serverUrl);

            // Create HTTP transport objects
            HttpRequestMessage httpRequest = null;
            try
            {
                #region Build Request Content

                // 27 dashes then current ticks as hexa decimal
                string partBoundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
                byte[] partBoundaryBytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + partBoundary + "\r\n");
                string formContentType = "multipart/form-data; boundary=" + partBoundary;

                MemoryStream requestStream = new MemoryStream();

                // Write each form item one by one

                //
                // -----------------------------7d81b516112482
                //
                // Content-Disposition: form-data; name="fname"
                //
                //
                // Anu
                //
                // -----------------------------7d81b516112482
                //
                // Content-Disposition: form-data; name="lname"
                //
                //
                // Chandy
                //

                string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
                foreach (string key in formData)
                {
                    requestStream.Write(partBoundaryBytes, 0, partBoundaryBytes.Length);
                    string formItem = string.Format(formdataTemplate, key, formData[key]);
                    byte[] formItemBytes = System.Text.Encoding.ASCII.GetBytes(formItem);
                    requestStream.Write(formItemBytes, 0, formItemBytes.Length);
                }

                requestStream.Write(partBoundaryBytes, 0, partBoundaryBytes.Length);

                // Write the file stream to upload

                // Content-Disposition: form-data; name="photo"; filename="myprofilephoto.jpeg"
                //
                // Content-Type: image/jpeg
                //
                //
                // file-content-goes-here......

                string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";
                string header = string.Format(headerTemplate, fileControlName, filePath, fileContentType);
                byte[] headerBytes = System.Text.Encoding.ASCII.GetBytes(header);

                requestStream.Write(headerBytes, 0, headerBytes.Length);

                FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
                byte[] buffer = new byte[4096];
                int bytesRead = 0;
                while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
                {
                    requestStream.Write(buffer, 0, bytesRead);
                }

                fileStream.Close();

                byte[] trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + partBoundary + "--\r\n");
                requestStream.Write(trailer, 0, trailer.Length);

                #endregion

                #region creates HttpRequestMessage object

                httpRequest = new HttpRequestMessage();
                httpRequest.Method = HttpMethod.Post;
                httpRequest.RequestUri = RequestUri;
                requestStream.Position = 0;
                httpRequest.Content = new StreamContent(requestStream);
                httpRequest.Content.Headers.Remove("Content-Type");
                httpRequest.Content.Headers.TryAddWithoutValidation("Content-Type", formContentType);
                httpRequest.Headers.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");

                #endregion

                #region Send the request and process response
                // Send Request

                HttpResponseMessage httpResponse = null;
                try
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    HttpClient httpClient = new HttpClient();
                    httpClient.Timeout = TimeSpan.FromSeconds(300);
                    httpResponse = await httpClient.
                        SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);

                    HttpStatusCode statusCode = httpResponse.StatusCode;
                    if (statusCode != HttpStatusCode.OK)
                    {
                        string errorResponse = 
                            await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

                        throw new Exception(errorResponse);
                    }
                }
                finally
                {
                    if (httpResponse != null)
                    {
                        httpResponse.Dispose();
                    }
                }

                #endregion
            }
            finally
            {
                if (httpRequest != null)
                {
                    httpRequest.Dispose();
                }
            }
        }

No comments:

Post a Comment