Handling (and squashing) large file uploads in ASP.Net

November 17, 2009
Sean Cooper

File under: Too Big

Not too long ago, Rob, Rich, and I got onto the subject of building a secure website and everything that goes into it. The fruits of that discussion are going to be saved for another article. However, one thing we did get to talking about was allowing users to upload files and how to ensure they don't go over the size limit we set for them.

By default, ASP.Net allows files up to 4MB in size to be uploaded. If a file larger than 4MB is uploaded, IIS throws a (seemingly untrappable) error and your client gets a white screen telling them the site is unavailable. I don't know about you, but I'd like to give something a bit better than a default error screen if they go over the limit. While you can certainly raise the size limit, the problem arises whenever someone tries and uploads a file larger than what you've specified.

So, just how do you handle a client who's trying to post a 50MB file to your site? How do you tell them "No!" nicely and keep your application running? The answer is an HTTPModule.

How do you eat an elephant?

One bite at a time.

That's the best analogy I can think of as to how we're going to handle our very large file. In general, whenever the request object is referenced, the whole thing is loaded by IIS, if its too large, the fatal error is thrown. However, our HTTPModule will look at the size of the request, parse through the full request, taking it a chunk at a time, and then send us off to an error page where we can tell the user that they tried to upload a file that was too darn big.

Setting the Table: The Configuration

This solution requires a couple small changes to the web.config file of your web app. First, in the <httpModules> ection, add the following line:
<add name="UploadModule" type="SecureDocDemo.UploadModule"/>. This registers the module within your app and allows it to run.

Also, add <httpRuntime executionTimeout="180" maxRequestLength="4096"/> to your web.config. The executionTimeout value is in seconds, so we're going to wait for 3 minutes before raising a timeout event. The maxRequestLength value is in Kb. Here we've set our maximum request size to 4MB. Keep in mind these are all within the system.web section of your config. You can change the executionTimeout and maxRequestLength values to suit your needs.

Knife and Fork: Creating the HTTP Module

First, let's create a module. We'll call it Upload Module and we'll implement iHTTPModule. We won't do anything with the Dispose method the interface gives us but we will use the Init event handler to wire up the BeginRequest event of our HTTPApplication to the BeginRequest method we're about to write.


Namespace LargeFileHandler
    Public Class UploadModule : Implements IHttpModule
                       
        Public Sub Init(ByVal context As System.Web.HttpApplication) _
        Implements System.Web.IHttpModule.Init
            AddHandler context.BeginRequest, AddressOf BeginRequest
        End Sub  

The BeginRequest method starts like this.
Public Sub BeginRequest(ByVal source As Object, ByVal e As EventArgs)
    Dim app As HttpApplication = CType(source, HttpApplication)
    Dim context As HttpContext = app.Context
    Const DefaultBufferLength As Integer = 1024 * 2

    If InStr(context.Request.ContentType, "multipart/form-data") = 0 Then
        Exit Sub
    End If

Sizing up the Elephant: Looking at the Request object

Within our BeginRequest method we first need to get ahold of the HTTPApplication and then getting the context object from it. Once we have the context object, we'll need to check to see if the ContentType is "multipart/form-data". If it's not, we're going to exit out as they aren't uploading a file.
Assuming they are uploading a file, we need to get the HTTPWorkerRequest object using reflection.


Dim hwr As HttpWorkerRequest = CType(context.GetType. _
    GetProperty("WorkerRequest", Reflection.BindingFlags. _
    Instance Or Reflection.BindingFlags.NonPublic). _
    GetValue(context, Nothing), HttpWorkerRequest)  
   
Dim ContentLength As Integer
Dim Buffer As Byte()
Dim Received As Integer = 0
Dim TotalReceived As Integer = 0            

Try
    ContentLength = CType(hwr.GetKnownRequestHeader( _
        HttpWorkerRequest.HeaderContentLength), Integer)
    If ContentLength > GetMaxRequestLength(app) * 1024 Then
        Buffer = hwr.GetPreloadedEntityBody()
        Received = Buffer.Length
        TotalReceived += Received

Once we have the HTTPWorkRequest, we can really get to work. First, we'll get the size of our content. We're also going to declare a byte array called, creatively enough, Buffer as well as two variables, Received and TotalReceived to serve as counters. If the content length is greater than what we've set in our config, we know we have something to do. Here's where we start taking bites out of the elephant.

One Mouthfull at a Time: Consuming the Request in 2MB chunks

In a simple While loop, we are going to (re)initialize our buffer to our DefaultBufferLength, fill our buffer with data using the ReadEntityBody method of our HTTPWorkerProcess, then increment our counters. And we're going to do this as long as we have more than one buffer's worth of data left in our request.

Because we're re-initializing the Buffer at each interation of the loop, the data we'd read in previously is destroyed. Thus, we're not taxing our server memory and risking buffer overflows or other horrendous happenings. Think of it as giving the bite of the elephant to the dog under the table. And this dog has an unlimited appetite.


If Not hwr.IsEntireEntityBodyIsPreloaded Then
    'Read Data in DefaultBufferLength sized chunks
    While (ContentLength - TotalReceived) >= DefaultBufferLength
        Buffer = New Byte(DefaultBufferLength) {}
        Received = hwr.ReadEntityBody(Buffer, DefaultBufferLength)
        TotalReceived += Received
    End While

Don't forget to wipe your mouth

Once we've got down to the last bit of bytes, we'll make one last call to the ReadEntityBody, reinitialize our Buffer variable one more time to get rid of the last bite of the elephant, and send the user off to our error page.


 'Read the remaining data
            Buffer = New Byte(DefaultBufferLength) {}
            Received = hwr.ReadEntityBody(Buffer, (ContentLength - TotalReceived))
            TotalReceived += Received
        End If

        'cleanup and redirect
        Buffer = New Byte(DefaultBufferLength) {}
        context.Application("Result") = ""
        context.Response.Redirect("Upload_Error.aspx?size=" & ContentLength)
    End If
Catch ex As Exception

Finally
    Buffer = Nothing
End Try

Note: This solution is derived from code found on a discussion of how to successfully upload large files.

The Full Code


Imports Microsoft.VisualBasic
Imports System
Imports System.IO
Imports System.Web
Imports System.Reflection
Imports System.Web.Configuration

Namespace LargeFileHandler                   
 Public Class UploadModule : Implements IHttpModule

        Public Sub Dispose() Implements System.Web.IHttpModule.Dispose

        End Sub
       
        Public Sub Init(ByVal context As System.Web.HttpApplication) _
        Implements System.Web.IHttpModule.Init
            AddHandler context.BeginRequest, AddressOf BeginRequest
        End Sub

        Public Sub BeginRequest(ByVal source As Object, ByVal e As EventArgs)
            Dim app As HttpApplication = CType(source, HttpApplication)
            Dim context As HttpContext = app.Context
            Const DefaultBufferLength As Integer = 1024 * 2
           
            If InStr(context.Request.ContentType, "multipart/form-data") = 0 Then
                Exit Sub
            End If

            ' Get the HttpWorkerRequest Object
            Dim hwr As HttpWorkerRequest = CType(context.GetType. _
                GetProperty("WorkerRequest", Reflection.BindingFlags. _
                Instance Or Reflection.BindingFlags.NonPublic). _
                GetValue(context, Nothing), HttpWorkerRequest)
               
            Dim ContentLength As Integer
            Dim Buffer As Byte()
            Dim Received As Integer = 0
            Dim TotalReceived As Integer = 0 'initialize our counter
            Try
                ContentLength = CType(hwr.GetKnownRequestHeader( _
                    HttpWorkerRequest.HeaderContentLength), Integer)
               
                If ContentLength > GetMaxRequestLength(app) * 1024 Then
                    Buffer = hwr.GetPreloadedEntityBody()
                    Received = Buffer.Length
                    TotalReceived += Received

                    If Not hwr.IsEntireEntityBodyIsPreloaded Then
                        'Read Data in DefaultBufferLength size chunks
                        While (ContentLength - TotalReceived) >= DefaultBufferLength
                            Buffer = New Byte(DefaultBufferLength) {}
                            Received = hwr.ReadEntityBody(Buffer, DefaultBufferLength)
                            TotalReceived += Received 'increment our counter
                        End While

                        'Read the Remain Data
                        Buffer = New Byte(DefaultBufferLength) {}
                        Received = hwr.ReadEntityBody(Buffer, _
                            (ContentLength - TotalReceived))
                        TotalReceived += Received
                    End If

                    'Finish AnalysisRecievedData
                    Buffer = New Byte(DefaultBufferLength) {}
                    context.Application("Result") = ""
                    context.Response.Redirect("Upload_Error.aspx?size=" & ContentLength)
                End If
            Catch ex As Exception

            Finally
                Buffer = Nothing
            End Try

        End Sub

        Private Shared Function GetMaxRequestLength(ByVal inApp As HttpApplication) _
            As Integer
           
            Return CInt(CType(inApp.Context.GetSection("system.web/httpRuntime"), _
            HttpRuntimeSection).MaxRequestLength)
        End Function
    End Class  
End Namespace