-
Notifications
You must be signed in to change notification settings - Fork 24
Using Mach II Exception Handling
- Introduction
- Exception Process Walkthrough
- The
Exception
Object - Leveraging the Exception Handling Process
- List of Common Exception Types Thrown by Mach-II
- Additional Notes and Considerations
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>
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:
- The exception information is packaged into an instance of
MachII.util.Exception
. - A new event is created with its name specified by the
exceptionEvent
property. - The exception object is placed in the new event's args with key
exception
. - If the exception occurred while handling an event, the exception causing event is placed in the new event's args with the key
exceptionEvent
. - 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
. - The event-context is cleared of all queued events.
- All plugins with a point named
handleException
are called. - The exception event is announced.
- The exception event is handled like any other event.
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')
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.
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>
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.
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.
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>
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:
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. |
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. |
- 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 theonError
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.