Skip to content

Using Mach II Exception Handling

thofrey edited this page Apr 1, 2014 · 5 revisions

Table of Contents

  1. Introduction
  2. Exception Process Walkthrough
  3. The Exception Object
  4. Leveraging the Exception Handling Process
  5. List of Common Exception Types Thrown by Mach-II
  6. Additional Notes and Considerations

Introduction

Exception handling in Mach-II makes use of the same event model that normal event processing uses. Exceptions occurring in business logic should typically be caught and handled before reaching the framework level. However, the framework itself will throw some exceptions directly.

Some exceptions that may occur at the framework level:

  • Attempting to announce an event from a HTTP request that is not publicly accessible (i.e. <event-handler name="something" access="private">).
  • Attempting to announce an event from within the framework in which there is not a defined event-handler.
  • Improperly tested code (i.e. incorrect use of the public API, unhandled exceptions in your listeners, filters, plugins, etc.)

The great part of all of this is that exception handling is part of the framework and not something that you would have to build yourself. However, you can customize what Mach-II will do when an exception occurs by customizing your exception event-handler.

The exceptionEvent property defined in the Mach-II config file (mach-ii.xml) defines the event to announce when the framework catches an exception.

Example from a mach-ii.xml file:

    <mach-ii>
        <properties>
            <!--
            If an exception is caught by the framework,
            package the exception and announce the 'exception' event.
            -->
            <property name="exceptionEvent" value="exception" />
            ... additional properties ...
        <properties>

        <event-handlers>
            <event-handler name="exception" access="private">
                <!-- Handle an exception here. -->
            </event-handler>
        </event-handlers>
    </mach-ii>

Exception Process Walkthrough

Whether an exception is thrown by the framework itself or caught as a result of a business logic error, it will be handled in the same fashion. When an exception is caught by Mach-II, the following occurs:

  1. The exception information is packaged into an instance of MachII.util.Exception.
  2. A new event is created with its name specified by the exceptionEvent property.
  3. The exception object is placed in the new event's args with key exception.
  4. If the exception occurred while handling an event, the exception causing event is placed in the new event's args with the key exceptionEvent.
  5. If the exception occurred because the event handler is non-existent, the original event data is placed in the new event's args with the key missingEvent.
  6. The event-context is cleared of all queued events.
  7. All plugins with a point named handleException are called.
  8. The exception event is announced.
  9. The exception event is handled like any other event.

The Exception Object

The Exception object models an un-handled exception caught by the framework. The framework encapsulates the cfcatch and other data into a more manageable CFC so that developers can use it to build their own error handling system. The following information via getters is available in the Exception object:

Method Name Description
getType Gets the type of the exception.
getMessage Gets the message of the exception.
getDetail Gets the detail of the exception.
getErrorCode Gets the error code of the exception. Returns zero length string if not available.
getExtendedInfo Gets the extended information of the exception. Returns zero length string if not available.
getTagContext Gets the array of the tag call history up until the exception.
getCaughtException Returns the original cfcatch struct with additional information such as SQL if it is a database error. Returned data varies based on the type of exception.

Once an un-handled exception occurs, you can get the generated Exception object by calling in any Mach-II extended components or views:

    event.getArg('exception')

Leveraging the Exception Handling Process

handleException Plugin Point

You can use the handleException plugin point to catch a missing event handler and route it to a generic event handler. A common example is routing a specific event handler (which does not exist in the configuration xml) like admin.568.update to admin.update. The plugin point could use the data point of 568 to set an event arg named admin=568 so the generic event named admin.update can perform the specified operation. In the handleException plugin point you would call:

    eventContext.getNextEvent().getArg("missingEvent")

You use getNextEvent() because the handleException() plugin point is called before the next event begins processing.

Once the exception event starts processing, you can get at the original event data by calling:

    event.getArg("missingEvent")

The missingEvent was added in Mach-II 1.6 and is not available in previous releases.

Handling Undefined Events in the handleException Plugin Point

If a user mistypes a URL or otherwise requests an event that is not defined, you want to be able to handle this is a generic fashion. You can do this in the handleException plugin point. However, because this plugin point can be called before event processing begins you need to check for the existence of either the currentEvent or the nextEvent. A request for an event that cannot be found in the mach-ii.xml config will throw an error in the preProcess plugin point because the eventContext passed in as that argument won't be of type MachII.framework.EventContext (because the framework won't be able to match an event in the mach-ii.xml configuration to the invalid request from the user). You can check for either the current or nextEvent as follows:

    <cffunction name="handleException" access="public" returntype="void" output="true">
        <cfargument name="eventContext" type="MachII.framework.EventContext" required="true" />

        <cfset var event = "" />
        <cfset var missingEvent = "" />

        <cfif arguments.eventContext.hasCurrentEvent()>
            <cfset event = arguments.eventContext.getCurrentEvent() />
        <cfelse>
            <cfset event = arguments.eventContext.getNextEvent() />
        </cfif>

        <!---
        If it is due to the a missing event args and you'll want the
        missing event as well, you need to do:
        --->
        <cfif arguments.exception.getType() EQ "MachII.framework.EventHandlerNotDefined">
            <cfset missingEvent = event.getArg("missingEvent") />
        </cfif>

        <!---
        ... handle the missing event with a nice "Not Found" or "We Can't Find What
            You're Looking For" page if you can't decipher from the missing event what they were looking for
            handle real exceptions by logging the exception, emailing the developers,
            and presenting a nice view to the user...
        --->
    </cffunction>

Customizing the exception Event-Handler

Another common use of the exception handling is to show a nice error page to users when the application is deployed in a production environment and debugging/exception data to developers when the application is deployed in a development or staging environment. You can accomplish this by using an event-filter to announce the correct event based on where the application is deployed (simplest is sniffing the cgi.server_name) and then define exception.production and exception.development event handlers.

This can easily be accomplished by using a Mach-II event-filter and some additional event-handlers. In this example, we have two different modes of exception handling: development and production. Even though you may have additional deployment environments, you will probably only need these two types as either you need developer aimed data to aid in debugging or production like pages that have "nice" messages to the end-user. We are using a Mach-II 1.8+ feature to auto-decide which mode the exception filter should use:

Exception event-handlers:

    <event-handler event="exception" access="private">
        <filter name="exception"/>
    </event-handler>

    <event-handler event="exception.development.default" access="private">
        <view-page name="exception.development.default" contentArg="layout.content"/>
        <execute subroutine="doMainLayout"/>
    </event-handler>

    <event-handler event="exception.production.default" access="private">
        <view-page name="exception.production.default" contentArg="layout.content"/>
        <execute subroutine="doMainLayout"/>
    </event-handler>

    <event-handler event="exception.production.noEventHandler" access="private">
        <view-page name="exception.production.noEventHandler" contentArg="layout.content"/>
        <execute subroutine="doMainLayout"/>
    </event-handler>

You'll have to develop the views you want for each event-handler and any layout subroutines to display the exception information. So let's continue by defining our custom event-handler.

Defining the exception event-filter:

    <event-filter name="exception" type="WebAssess.filters.ExceptionFilter">
        <parameters>
            <parameter name="enableProductionExceptionHandling">
                <struct>
                    <key name="group:development" value="false"/>
                    <key name="group:production" value="true"/>
                </struct>
            </parameter>
        </parameters>
    </event-filter>

The enableProductionExceptionHandling parameter either takes boolean (true/false) or a struct of keys. If you define a struct to define the exception handling modes, you must being using Mach-II 1.8+. The struct corresponds to environment names or environment groups, which must be prefixed with group:, with a corresponding boolean. The key names may list multiple environment names / groups by defining a list (ex. <key name="group:production,staging,qa" value="true"/>).

Exception event-filter code:

    <cfcomponent
        displayname="exceptionFilter"
        extends="MachII.framework.EventFilter"
        output="false"
        hint="Performs exception filter actions.">

        <!---
        PROPERTIES
        --->
        <cfset variables.enableProductionExceptionHandling = true />

        <!---
        INITIALIZATION / CONFIGURATION
        --->
        <cffunction name="configure" access="public" returntype="void" output="false"
            hint="Configures the filter.">
            <!--- Decide if production exception handling should be enable based on environment --->
            <cfset setEnableProductionExceptionHandling(resolveValueByEnvironment(getParameter("enableProductionExceptionHandling"), true)) />
        </cffunction>

        <!---
        PUBLIC FUNCTIONS
        --->
        <cffunction name="filterEvent" access="public" returntype="boolean" output="false"
            hint="Writes to log and sends email of exception.">
            <cfargument name="event" type="MachII.framework.Event" required="true" />
            <cfargument name="eventContext" type="MachII.framework.EventContext" required="true" />
            <cfargument name="paramArgs" type="struct" required="false" default="#StructNew()#" />

            <cfset var exception = arguments.event.getArg("exception") />
            <cfset var caughtException = exception.getCaughtException() />
            <cfset var requestName = arguments.event.getRequestName() />
            <cfset var exitEvent = "" />

            <!--- Decide if production handling is to be used --->
            <cfif isEnableProductionExceptionHandling()>
                <cfswitch expression="#caughtException.type#">
                    <!--- Announce event for "bad events" such as spammers messing with URLs or wrongly typed URLs --->
                    <cfcase value="MachII.framework.EventHandlerNotDefined|MachII.framework.EventHandlerNotAccessible|MachII.framework.ModuleNotDefined|MachII.framework.UrlRouteNotDefined" delimiters="|">
                        <cfset arguments.eventContext.addHTTPHeaderByStatus("404", "Not Found") />
                        <cfset exitEvent = "exception.production.noEventHandler" />
                    </cfcase>
                    <!--- Announce event for completely unhandled exceptions  --->
                    <cfdefaultcase>
                        <cfset arguments.eventContext.addHTTPHeaderByStatus("500", "Error") />
                        <cfset exitEvent = "exception.production.default" />
                    </cfdefaultcase>
                </cfswitch>
            <cfelse>
                <!--- Announce event with "nice" developer exception info --->
                <cfset arguments.eventContext.addHTTPHeaderByStatus("500", "Error") />
                <cfset exitEvent = "exception.development.default" />
            </cfif>

            <cfset announceEvent(exitEvent, arguments.event.getArgs()) />

            <cfreturn true />
        </cffunction>

        <!---
        ACCESSORS
        --->
        <cffunction name="setEnableProductionExceptionHandling" access="private" returntype="void" output="false">
            <cfargument name="enableProductionExceptionHandling" type="boolean" required="true" />
            <cfset variables.enableProductionExceptionHandling = arguments.enableProductionExceptionHandling />
        </cffunction>

        <cffunction name="isEnableProductionExceptionHandling" access="private" returntype="boolean" output="false">
            <cfreturn variables.enableProductionExceptionHandling />
        </cffunction>

    </cfcomponent>

As the code shows when the application is deployed in a production environment, the event-filter snoops the exception type to determine what exception event handler should be announced. Whereas when the application deployed in development environments, we announce a developer aimed event-handler.

Sending Proper HTTP Status Codes

Since Mach-II is handling all of the requests through the index.cfm file (i.e. front controller handling), your web server will always send out an 200 OK HTTP status code for all requests. This is not ideal when dealing with search engine spiders that may request out dated URLs (i.e. URLs with incorrect event handler names in them). Search engine spiders will repeatedly re-request old URLs that are processed by your exception handling process if a 200 OK is always sent back because the spider thinks this is a valid URL target. Be sure to always send a 404 Not Found back for event handlers that are not found or not publicly accessible (i.e. <event-handler event="something" access="private">). For exceptions that are not handled by your code (i.e. syntax error, DB error, lock timeouts, etc.), be sure to send back a 500 Error status code so the spider will retry this URL again in the future. The above code demonstrates sending out proper HTTP status codes.

Also note well, we are using the publicly available methods in the EventContext to send the proper statuscode codes back instead of using the cfheader. This is especially important if you are using Mach-II caching as the caching package does record headers created through these methods and replays them back when using cached data. The caching package does not replay headers created with cfheader because the EventContext methods act as wrappers for cfheader and they observe the headers that you want to send so they can be replayed in future requests if using cached data. This also applies to calls to cfhtmlheadelement be sure to use addHtmlHeadElement() method.

Using Event-Mappings to Route Exceptions to Specialized Exception Event Handlers

You can use an event-mapping to map an exception from your default exception event handler to a specialized event handler. This is only useful in certain circumstances (it's not recommended to do this in all events), but is very useful in certain situations.

    <event-handler event="doSomethingImportant" access="public">
        <event-mapping event="exception" mapping="doSomethingImportant.exception" />
        <notify listener="something" method="doVeryImportant" />
    </event-handler>

    <event-handler event="doSomethingImportant.exception" access="private">
        <!-- Logic to handle this special exception -->
    </event-handler>

List of Common Exception Types Thrown by Mach-II

There are several exception types that can be thrown by the framework itself. Below is a list of types and the situations in which they can be thrown:

Common Exceptions When Application is Deployed to Production

All of the below exceptions can occur when an user mistypes an URL or clicks on a link with an incorrect of out-dated event-handler in it. Technically none of the exceptions above are all the exceptional in nature because they all map to a 404 Not Found HTTP status code. All of these exceptions should be covered by your exception handling so you can present a friendly exception page the user.

Type Name Reason(s)
MachII.framework.ModuleNotDefined This exception can occur when an user agent (browser) requests an event in a module that does not exist. For example if a request for the index event-handler in the account module (i.e. index.cfm?event=account:index) is made and your application does not have a module named account, this exception would be raised by the framework.
MachII.framework.EventHandlerNotDefined This exception occurs when a requested event-handler does not exist. Common reasons for this exception are mistyped URLs in anchor href or mistyped URLs by users in the browser URL bar.
MachII.framework.EventHandlerNotAccessible This exception occurs when a requested event-handler cannot be invoked directly from a URL (i.e. <event-handler event="something" access="private">). Such event-handlers are common for events that should only be programmatically announced from within the framework code (i.e. listener, plugin or filter), but not directly via an URL in the browser. The best security practice is use the same event-handler as EventHandlerNotDefined and treat direct requests to these event-handlers via a browser as 404 Not Found. This way nobody can even guess if an event-handler with an access modifier of private event exists.
MachII.framework.UrlRouteNotDefined This exception occurs if an user tries to request an URL route that is not defined in your application.

Common Exceptions When Application is Being Developed

All of the below exceptions can occur during the development of your application. Usually, these exceptions are normally discovered during the development of your application and are corrected before being deployed to production. These types of exceptions are truly exceptional in nature and should be logged/reported (via a Mach-II cflog logger and/or email logger) since these types of exceptions indicate a developer error and should be corrected in future versions of your application. The best practice is to treat this an un-handled exception and send a 500 Error status code to the user with a friend error page.

Type Name Reason(s)
MachII.framework.MaxEventsExceededDuringException This exception occurs when the maximum number of events exceeds the value listed in the maxEvents property. This property is used to stop infinite loops from occurring due to programmer error.
MachII.framework.RedirectRouteArgsInvalidDatatype This exception occurs if you send an incorrect datatype in the args argument when using the redirectRoute method available in your listeners, filters and plugins. This indicates a syntax error using the method so check the arguments you are passing it against the API.
MachII.framework.RedirectRoutePersistArgsInvalidDatatype This exception occurs if you send an incorrect datatype in the persistArgs argument when using the redirectRoute method available in your listeners, filters and plugins. This indicates a syntax error using the method so check the arguments you are passing it against the API.
MachII.framework.RedirectEventArgsInvalidDatatype This exception occurs if you send an incorrect datatype in the args argument when using the redirectEvent method available in your listeners, filters and plugins. This indicates a syntax error using the method so check the arguments you are passing it against the API.
MachII.framework.RedirectEventPersistArgsInvalidDatatype This exception occurs if you send an incorrect datatype in the persistArgs argument when using the redirectEvent method available in your listeners, filters and plugins. This indicates a syntax error using the method so check the arguments you are passing it against the API.
MachII.framework.invalidHTTPHeaderArguments This exception occurs if you pass an invalid combination of arguments to the EventContext? method addHTTPHeader. Valid argument combinations are 'name,value' or 'statusCode'.
MachII.framework.eventMappingModuleNotDefined This exception occurs if you try to create an event-mapping to a module that does not exist in your application. Check to make sure that the module exists and that you spelled the module name correctly.

Additional Notes and Considerations

  • Exception handling cannot handle exceptions that occur during the loading of the framework. This is catch-22 situation because the exception handling requires the framework to be loaded before it can process exceptions. This means it is the responsibility of the developer to ensure their application loads which is a basic requirement for developing any application whether it is built on Mach-II or not. A simple <cferror> or using the onError method in your Application.cfc is the most common way to ensure nice and pretty exception pages are shown to users in the event of a catastrophic failure in your application.
  • The missingEvent was added in Mach-II 1.6 and not available in previous releases.
Clone this wiki locally