Eduardo Soto Eduardo Soto - 2 months ago 50
C# Question

How to parse XHR multipart/form-data request with multiple files

I have seen multiple examples online of multipart/form-data parsers but none of them work on my site. I am using a Kendo Upload control set to async mode and with batch upload enabled, the generated request looks something like this:

BOUNDARY
Content-Disposition: form-data; name="Param_1"

Param_1 Value
BOUNDARY
Content-Disposition: form-data; name="Param_2"

Param_2 Value
BOUNDARY
Content-Disposition: form-data; name="files[]"; filename="A.docx"
Content-Type: application/octet-stream

[Binary Data Here]
BOUNDARY
Content-Disposition: form-data; name="files[]"; filename="B.docx"
Content-Type: application/octet-stream

[Binary Data Here]
BOUNDARY--


Every library I have found online successfully retrieves the parameters and the first file, but some never see the second and the ones that do save it incorrectly. There is a similar question on SO WCF multipart/form data with multiple files but that solution only works for text files not binary files.

Answer

The problem other solutions had with binary files was that they converted the response into a string to parse it and then converted that string back into the file, this does not work with binary data. I came up with a solution where instead of turning the response into a string to parse it, I left it as a byte[] and split it by the delimiter as a byte[] using the code found here. Once that's done convert each piece to a string to see if it is a parameter or a file, if it is a parameter then read it otherwise write it as a file. Here is the working code that assumes you have the Stream as a byte[] and the delimiter as a string:

// given byte[] streamByte, String delimiterString, and Encoding encoding
Regex regQuery;
Match regMatch;
string propertyType;

byte[] delimiterBytes = encoding.GetBytes(delimiterString);
byte[] delimiterWithNewLineBytes = encoding.GetBytes(delimiterString + "\r\n");
// the request ends DELIMITER--\r\n
byte[] delimiterEndBytes = encoding.GetBytes("\r\n" + delimiterString + "--\r\n");
int lengthDifferenceWithEndBytes = (delimiterString + "--\r\n").Length;

// seperate by delimiter + newline
// ByteArraySplit code found at http://stackoverflow.com/a/9755250/4244411
byte[][] separatedStream = ByteArraySplit(streamBytes, delimiterWithNewLineBytes);
streamBytes = null;
for (int i = 0; i < separatedStream.Length; i++)
{
    // parse out whether this is a parameter or a file
    // get the first line of the byte[] as a string
    string thisPieceAsString = encoding.GetString(separatedStream[i]);

    if (string.IsNullOrWhiteSpace(thisPieceAsString)) { continue; }

    string firstLine = thisPieceAsString.Substring(0, thisPieceAsString.IndexOf("\r\n"));

    // Check the item to see what it is
    regQuery = new Regex(@"(?<=name\=\"")(.*?)(?=\"")");
    regMatch = regQuery.Match(firstLine);
    propertyType = regMatch.Value.Trim();

    // get the index of the start of the content and the end of the content
    int indexOfStartOfContent = thisPieceAsString.IndexOf("\r\n\r\n") + "\r\n\r\n".Length;

    // this line compares the name to the name of the html input control, 
    // this can be smarter by instead looking for the filename property
    if (propertyType != "files")
    {
        // this is a parameter!
        // if this is the last piece, chop off the final delimiter
        int lengthToRemove = (i == separatedStream.Length - 1) ? lengthDifferenceWithEndBytes : 0;
        string value = thisPieceAsString.Substring(indexOfStartOfContent, thisPieceAsString.Length - "\r\n".Length - indexOfStartOfContent - lengthToRemove);
        // do something with the parameter
    }
    else
    {
        // this is a file!
        regQuery = new Regex(@"(?<=filename\=\"")(.*?)(?=\"")");
        regMatch = regQuery.Match(firstLine);
        string fileName = regMatch.Value.Trim();

        // get the content byte[]
        // if this is the last piece, chop off the final delimiter
        int lengthToRemove = (i == separatedStream.Length - 1) ? delimiterEndBytes.Length : 0;
        int contentByteArrayStartIndex = encoding.GetBytes(thisPieceAsString.Substring(0, indexOfStartOfContent)).Length;
        byte[] fileData = new byte[separatedStream[i].Length - contentByteArrayStartIndex - lengthToRemove];
        Array.Copy(separatedStream[i], contentByteArrayStartIndex, fileData, 0, separatedStream[i].Length - contentByteArrayStartIndex - lengthToRemove);
        // save the fileData byte[] as the file
    }

}