Home | Wiki | OI 1.x Docs | OI 2.x Docs |
Error Handling in OpenInteract
Error handling in any individual application can get a little tricky. Creating a framework that accommodate all types of applications is much trickier!
We've tried to navigate the shoals of flexibility, robustness and completeness. This solution does not entirely satisfy all three critieria, but it should do well enough in all to create applications that can handle anything that comes their way. It should also be extensible enough so that if you see a shortcoming, you can fix it yourself fairly easily.
First: every error thrown is an object. This object has a number of parameters which can be set in two different ways (more on that below).
Error Parameters
This object can ``do'' a few actions: notify someone that it was raised and its details, save itself into a file, log or database. We don't necessarily want to log every error, however. Otherwise we will get a lot of fairly useless errors that will wind up obscuring the meaningful stuff.
Since an error object is just another SPOPS object, you can do anything with it that you do with a SPOPS object:
my $err = OpenInteract::Error->new; $err->{code} = 718; $err->{user_msg} = "Press the 'any' key"; $err->{system_msg} = "User is a dingdong"; $err->{type} = 'user error'; $err->save;
The only thing different about this object than any other is that the error object registers a handler that takes hold if it fails to save properly that dumps the error to the filesystem for review. These errors are dumped into the directory labeled 'error' in the configuration and are currently not part of the error browser tool. (Other SPOPS objects can do this, of course, but the Error object is the only one that comes this way out of the box.)
You can also set error messges to be used in a later throw. There are two reasons for this. First, the OpenInteract framework is tied fairly closely to the SPOPS data abstraction layer. An error in SPOPS is handled by saving all relevant information about the error into package variables in the SPOPS::Error namespace. A brief message about the error is then sent back to the user through the use of die, and the detailed information can be retrieved through the get() class method. For instance:
my $id = eval { $object->save }; if ( $@ ) { my $info = SPOPS::Error->get; while ( my ( $k, $v ) = each %{ $info } ) { print "Error info -- $k: $v\n"; } }
Since OpenInteract uses something suspiciously similar, you might see the following idiom many times:
my $id = eval { $object->save }; if ( $@ ) { Interact::Error->set( SPOPS::Error->get ); $R->throw( { code => 404 } ); }
Which basically just says: ``take all the error information from SPOPS, send it over to Interact and throw an error that uses that information by default.''
Similarly, you might wish (for readability and consistency) to use the same idiom even when the error is not being thrown by SPOPS:
eval { sendmail( %msg ) || die $Mail::Sendmail::error }; if ( $@ ) { Interact::Error->set( { system_msg => $@, type => 'email', user_msg => 'Cannot send email', extra => \%msg } ); $R->throw( { code => 544 } ); }
You can always throw an error with all the information passed explicitly as well (more below). It's all up to you.
The error handling framework uses these objects to determine proper actions resulting from them. Here's how that works:
We saw an example of what throwing an error looks like above, but now we'll just define all the parameters explicitly rather than using the get()/set() class method syntax. (Note that you can execute the throw() method using the object class or, for convenience, from $R.)
eval { $class->open_file( $filename ) }; if ( $@ ) { $R->throw( { code => 444, type => 'handler', user_msg => 'Could not retrieve list of groups.', system_msg => "Error: $@", notes => { filename => $filename } } ); }
Note that you do not need to throw an error every time an error might occur. (Otherwise your applications might get a little complicated...) Generally, you only want to throw an error when you either want to let the system keep track of a particular occurrence, when you want the system to take control of the current request, or when you want to report a message back to the user.
You can also throw an error when you're in the development stage to track the status of a task, and remove the error throw when you move to production code.
The code that handles the error can be arbitrarily simple or complex. The code can define new content / user_interface handlers for the current request or generate its own content and skip the content handler phase altogether.
The error handler also decides whether to save() the error object or not. Many times you do not wish to save an error since it might be very common, thus cluttering up the log and obscuring real problems.
The code that handles the error can decide to return to the existing process or short-circuit and skip to the next step. For instance, if during a content handler I get an error with the database connection, I likely don't want to go any further in the operation since I'll keep getting errors. In that case, my error handler should issue a generic 'die;' command.
An eval {}; block will capture any die; commands within the handler (or any of its called procedures) and allow you to skip the remainder of the handler while still going on to the next step.
If the handler returns without calling die(), it can return:
The hashref can include any sort of information your application needs.
Practically speaking, you almost always either return nothing or
return a hashref of useful information. If you want something to be
displayed, just die()
with it.
The handler can also 'leave messages' for later handlers or components. For instance, if a user logs in with incorrect credentials, the error handler can leave a message for the 'login_box' component. When the 'login_box' component is called, it has access to its error messages in the template (the implementation TBD) and can use the messages as it wishes.
To ensure the messages do not step on each other, they are stored in a fairly deep hierarchy. Here's a detail of each step:
The second level (hashref stored in ($R->{ $ERROR_HOLD }) is always available to the templates under the variable name 'error_hold'. So your template might look something like this:
[%- IF error_hold.loginbox.bad_login %] <tr align="center"> <td colspan="2"><font color="#ff0000"> [% error_hold.loginbox.bad_login %] </font></td>> </tr> [% END -%]
Where 'loginbox' is the name of the component, and 'bad_login' is the place the template will look for a particular messge. Further refinement might modify this so that the variable 'error_hold' only has the messages destined for that particular component.
As noted below, the system reserves the first 25 entries at each severity level for system-level messages and errors. But what if you want to write your own error handler?
For instance, say you want to know when a particular user
We mentioned this briefly above, but it's worth going over again.
SPOPS, the data abstraction layer used on OpenInteract, deals with all
errors by setting error information in SPOPS::Error package variables,
then calling die()
with a simple message indicating the nature of the
error.
The reason for this separation is twofold. First is a design decision: SPOPS, while it was developed in tandem with OpenInteract, is a separate package which cannot have any dependencies on OpenInteract. Doing so would give is a nightmare. Second, the errors thrown by SPOPS cannot know any context -- SPOPS::DBI might know if you were trying to save something or remove something, but it doesn't know how to redirect an error if found. You might wish to throw a different error if a user record is not created versus whether a news object is not created.
The list of information in SPOPS::Error is shorter than that tracked in Interact::Error, since SPOPS::Error does not need to know user context, browser being used, session information, etc.
Severity is based on the error code. The lower the code, the more severe. The levels are modeled after syslog and many other unix programs.
The system reserves the first 25 entries in each error code level. You can override it if you wish, but you'd better know what you're doing.
Severity levels:
(incomplete)
Database errors ('db')
Examples
Authentication errors ('authenticate')
Examples
Authorization errors ('authorize')
Examples
Security errors ('security')
Infrastructure ('infrastructure')
Examples
Others?
Chris Winters (chris@cwinters.com)