NAME

Error Handling in OpenInteract


BASICS

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.


ERROR == OBJECT

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

code
Number indicating severity/handler

type
Something like db/security/authenticate/email/...

system_msg
Additional info for the admin, including detailed error messages.

user_msg
Brief description, suitable for showing to a user. (Not too detailed.) This is not the message that must be shown to a user, however, since the error handler is free to tell the user whatever it wishes.

package
From what package was the error raised?

file
From what filename was the error raised? (Usually this will be just about the same as the package, but you can not only have more than one package per file, you can have a package created entirely dynamically.)

line
From what line of code was the error raised?

module
From what module was the error raised?

user_id
Who made the request?

session_id
What session was the request using?

browser
User agent as given by the browser

time
when was the error thrown?

notes
Additional stuff, including more detailed information than is in system_msg (SQL statements, etc.)

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.)


ERROR MESSAGES

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.


ERROR HANDLING FRAMEWORK

The error handling framework uses these objects to determine proper actions resulting from them. Here's how that works:

  1. When an error is first raised (or 'thrown'), we create an object and put the passed parameters into it, put other parameters (browser, user/session_id, time, calling information). We do not save it to the database or filesystem yet.

  2. We pass the error object to the error dispatcher. This is a system handler which knows about the different error codes and the actions to take based on information from the server configuration. The error dispatcher finds out from the request object ($R) what module we're currently in so it can determine the proper error handlers to use.

  3. The dispatcher then queries each of the error handlers specified by the module to see if it can deal with the error. If it can deal with the handler, it returns a code reference that is then called by the dispatcher, with the error object sent as the first (and only) argument. If a coderef is not returned by the series of classes that act as error handlers, the dispatcher calls the system handler, which can catch and deal with all errors.

Throwing an Error

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.


ERROR HANDLERS

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:

undef
code can continue, no change to display

scalar
code can continue, but return value of procedure is requested to be scalar, and most of the time you will simply return the scalar

hashref
code can continue and output should be augmented by information in hashref; often the output can be simply dereferenced and put into the parameters to be interpolated into your template.

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.

Leaving Messages

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:

$ERROR_HOLD ($)
Package variable $OpenInteract::Error::ERROR_HOLD, also exported on request from OpenInteract::Error

$R->{ $ERROR_HOLD } (\%)
Hashref whose keys are the components that will use the error messages.

$R->{ $ERROR_HOLD }-{ $component }>. (\%)
Hashref whose keys are the tags for the actual error messages.

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.

Specifying a Custom Error Handler

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


SPOPS ERRORS

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.


ERROR CODES

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:

  1. 000 - 099
    emerg: system is unusable

  2. 100 - 199
    alert: action must be taken immediately

  3. 200 - 299
    crit: critical conditions

  4. 300 - 399
    err: error conditions

  5. 400 - 499
    warning: warning conditions

  6. 500 - 599
    notice: normal but significant condition

  7. 600 - 699
    info: informational

  8. 700 - 799
    debug: debug-level messages


ERROR TYPES

(incomplete)

Database errors ('db')

Examples

Authentication errors ('authenticate')

Examples

Authorization errors ('authorize')

Examples

Security errors ('security')

Infrastructure ('infrastructure')

Examples

Others?


AUTHORS

Chris Winters (chris@cwinters.com)