Sequel Error Handling

May 11 2010

Last time I talked about using exceptions with Sinatra’s error handling to easily format resource errors without having naming schemas or global variables. When doing work with exceptions it is up to you to draw a firm line on what an exceptional circumstance is. If you get a code review and the feedback is, “The code is terrible! How could you use exceptions like this. Java Developer!” Then they may be parroting something they read in fad magazine which was originally published 10 years ago, or, more likely, the proper feedback that you should hear is, “Your code is not terrible, but bad. The execution path is extremely confusing and in 2 months you won’t be able to follow it either.” From there you have two options. Redefine what an exceptional circumstance is in your domain, or figure out what is causing the yo-yoing in between the exception and the normal program flow.

One area I have found my exception code to be confusing is when dealing with user input. I like to say that if the input supplied to the program is not exactly what the program excepts, then it’s an exception! It helps more then you would believe in mitigating bugs. If you forget to filter the parameters in one of your resources, then it will crash instead of continuing on till your untimely doom! Unfortunately to make things user friendly you have to try to understand what the user intended in an exception, and then jump back up to the main program which, can mean quite a bit of Yo-Yoing. I find there to be a third case in-between normal execution path and exception code: a massaging loop! The massage code takes input that may not be exactly what the system is looking for and massages it into the proper format.

To implement this, you can either call a massage (aka filter) method before each system call. Or you can place the filter in an exception. The downside with the filter method is that you have to place it everywhere. You may not always want the same filters so you end up writing sub filters and checks in the control code which, on a successful scenario, aren’t run. Unfortunately, placing filters in exceptions is not a better solution because it’s confusing as you have just made control logic part of your exceptional circumstances.

So enough build up! I saw a video on Erlang, and I remembered reading a post about suppling a block to network functions to handle errors (my apologies but I could not find that post to site.) In the video it showcases how you can provide context specific error handlers to a function which can immediately correct expected errors for that function! My domain was specifically for handling input errors which caused validation failures. In the future there may be a more general purpose tool, but for now BlockErrorHandling is a Sequel plugin on my branch. Hopefully the maintainers of Sequel will find it useful enough to accept my pull.

In Sinatra, any errors that occur which are not rescuable we are going to want to report on. ProcErrorHandling (PEH) lets you supply a block which gets called with the failed model.

            ...
            Sequel::Model.plugin :proc_error_handling
            Sequel::Model.on_error { |bad_model| ENV['resource_error'] = bad_model }
            ...
            
            post '/posts' dojj
              @post = Post.create(params[:post])
              { status => 201 }.to_json
            end
            
            error Sequel::ValidationFailed do
               ENV['resource_error'].errors.to_json
            end
          

Very simply, Post is a Sequel::Model. Any validation errors which are raised from Post.create will be passed back to the user (12). The on_error (3) is inherited for each model and is called if Post.create fails. We can also specify individual on_error handlers for models which inherit from Sequel::Model.

            Sequel::Model.plugin :proc_error_handling
            Sequel::Model.on_error { |bad_madel| #Default catch all }
            Post.on_error { |bad_model| #Catch all for Posts only }
          

So that’s pretty cool, but now let’s say that a post is tied to an account, and an overzealous user supplies their account_id when creating. It’s not really an error, but account_id is (or should be) a restricted column. You can turn off the error Sequel raises when updating restricted columns, but this leaves your code open for bugs, possibly security ones, when doing mass updates where you forget to do some checking to make sure the account_id is proper. You could catch the error, remove the account_id and retry, but this makes the code hard to follow if it happens often. Proc error handling has another way!

            ...
            Sequel::Model.plugin :proc_error_handling
            def filter_restricted
              proc do |klass,values|
                if $!.message =~ /restricted/
                  values.delete_if { |k,v| klass.restricted_columns.include k }
                  :retry
                end
              end
            end
            ...
            
            post '/posts' do
              @account = account_from_token(params[:token])
              @post = Post.create(params[:post], filter_restricted) { |m| m.account = account }
            end
          

The filter_restricted (3) method returns a proc object which checks the error message being raised (4) and if it is about restricted columns it deletes all restricted columns from the hash provided (43), and then retry’s the method (44). This will handle the error by ignoring restricted columns for any model in which it is used.

You may return 4 things from an error handling proc. 1) If the proc returns nil then the error is passed up through the stack, or to the next error handling proc specified. You may supply N error handling procs as the last arguments of the database oriented methods (see the code for the list of methods). 2) If the proc returns :raise, the error is immediately raised an no other procs are called. 3) If the proc returns :retry, the operation that failed is retried. This is useful when modifying the values passed into the block. It is up to the user to handle infinite loops as if you retry but do not handle the error properly that block will keep getting called. 4) If the proc returns an object of the same type as Klass, that object is returned. When using the proc error handling in #new, the object returned is copied into self. 5) If the proc returns anything else an error is raised. The reason for doing this is the purpose of PEH is to massage the error back into the expected condition. If you return another object, you then have to have conditionals on what to do when create doesn’t return an instance of the class. Not only does this completely break the contract of #create, it also puts you right back to confusing yo-yoing on errors.

The next update to proc_error_handling is going to have constructors which work with the validations plugin. This will allow you to write blocks without figuring out what the error message is.

            ...
            Post.plugin :proc_error_handling
            def original_on_duplicate
              Post.on_unique_error { |klass,values| Post.first("email = ?",values[:email]) }
            end
            ...
            
            post '/posts' do
              @post = Post.create(params[:post], original_on_duplicate)
            end
          

Massage that user input!

Check out the diff on github!