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