A client of mine needed an admin tool for their site: a basic file upload script. Anyone who has ever written such a script knows that there are at least a good half dozen potential failure points when writing such a thing. Experience suggests that you should check at a minimum the following types of things:
For some of these tests, it may actually be pretty important to fail loudly enough for the user to notice. And of course, if we want to build RESTful web applications, it would be good to return the correct HTTP response code in a failure scenario.
While writing the script, using Camping, it suddenly occurred to me that with a bit of tooling, I could easily use Ruby’s exception handling tools to make my life easier, and to make the code maintainable. This technique can easily be applied to Rails applications too.
The Pickaxe, in its description of the Exception
class, describes a #status
method that is only available to the SystemExit
exception. That looks like a useful idea, but I need the status baked into the base Exception class. Why? Well, if the script is going to die because a directory isn’t writable, or for some other reason I couldn’t anticipate, I need the script to return a 500 error code. Time to monkey patch Exception
:
class Exception
def status
500
end
end
Now let’s extend this for some other error scenarios, like a user attempting to upload a file larger than I’d like:
class ForbiddenException < RuntimeError
def status
403
end
end
You should compile a big list of these exceptions into its own file and require
it in your app.
Ok, so now you’re in Camping (or Rails), and you’ve got your file processing logic sitting in a class in the model, you have an ‘error’ view (among others) and you’re calling this code from the controller. In the model, you might have a #save
method, and one of the things you’re going to do before you save your uploaded file is check the file size:
raise( ForbiddenException.new, "File Too Large!") if @file.size > 11000000
Now for the juicy bit: the controller logic:
class Index
def Post
file = FileUpload.new(...)
page = :index
begin
@file = file.save
rescue Exception => @e # oh, noes! my bad!
@status = @e.status
page = :error
end
render page
end
end
In Camping, the @status
variable is reserved for setting the HTTP response code, and is smart enough to change it from 200 to 302 when you’re redirecting; that said, you can set it to other codes as we’re doing here.
And the view (the @e
is being used here):
def error
h1 "An Error has occurred"
p @e
end
After getting this working, there’s two things I couldn’t believe:
One of the niftiest (yes, I like to use the word nifty) things about this method is that you could catch different kinds of exceptions and customize the output accordingly:
def Post
file = FileUpload.new(...)
page = :index
begin
@file = file.save
rescue ForbiddenException => @e # silly user!
@status = @e.status
page = :forbidden_page
rescue Exception => @e # oh, noes! my bad!
@status = @e.status
page = :generic_error
end
render page
end
With only a handful of lines, I managed to create a system that easily handled error scenarios in a maintainable fashion, and passed them off to the correct view as required. Better still, I managed to easily ensure that the client was receiving the correct HTTP response code. Are there ways this could be improved still more? Let me know.
Copyright © 2009
Robert Hahn.
All Rights Reserved unless otherwise indicated.