Experimenting With RESTful Error Response Codes And CFThrow's ErrorCode Attribute

Posted May 25, 2012 at 9:07 AM by Ben Nadel

Tags: ColdFusion

Earlier this week, in my comments about understanding MVC (Model-View-Controller) architecture in ColdFusion, I was describing my API workflow to Steven Neiland. When processing a RESTful request, I tend to use a CFTry-CFCatch-CFThrow workflow that short-circuits request processing using explicit exceptions. This workflow prevents the request from doing any extraneous processing by failing fast. As I was describing the workflow, however, I realized that my approach doesn't give the 3rd party API consumers much information to work with; since I have to squeeze a large number errors into a small subset of HTTP Status Codes, the cause of the error becomes a bit ambiguous. As such, I wanted to look into providing more error-specific information.


 
 
 

 
  
 
 
 

Before I started playing with code, I did some Google searches to see what other big API providers were doing. By far, the best example I found was on the Twilio website. Twilio maintains a long list of error codes that [seemingly] covers the entire set of possible errors that their API might return.

These error codes give the 3rd party API consumer-developer a lot of insight into what exactly is going wrong with the request. It's one thing to get a "400" response; it's another thing to get a "400" response because the "voice" attribute of the "Say" XML tag is invalid. With that kind of information, not only is debugging easier; but, it also becomes much easier to provide a user-friendly error message to any users of the 3rd party API client.

In order to facilitate this approach, we need a central list of possible errors as well as a way to map exceptions onto error codes. Luckily, ColdFusion's CFThrow tag has an "errorCode" attribute for this very purpose. I've never used it before, but the errorCode attribute appears to accept any simple value. This value can then be retrieved from the exception object within the context of a CFCatch tag.

As far as organizing the list of possible error codes, I didn't know what the best move would be; so, for the following demo, I'm just inlining it as a ColdFusion struct within the API processing workflow.

The following demo / exploration processes a single, implicit resources that expects a Name and Age attribute. If either of these values are missing or are invalid, individual exceptions will be raised.

Index.cfm - Our Primary API Entry Point

  • <!---
  • For this demo, we are only going to deal with a single
  • "resource" that accepts NAME and AGE values. We don't want to
  • param them here so that we can return more granular data in
  • the error response.
  •  
  • param url.name
  • param url.age
  • --->
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Set up some default values in the request. This data will help
  • us determine what kind of response we ultimately want to return
  • to the client.
  • --->
  •  
  • <!---
  • The status code will mimic the HTTP status code (in case the
  • status code is being suppressed for clients that can't handle
  • non-200 responses.
  • --->
  • <cfset request.statusCode = 200 />
  • <cfset request.statusText = "OK" />
  •  
  • <!---
  • The Response key is the resource payload being returned from the
  • API. This is the ONLY KEY that will be OVERRIDDEN directly by the
  • resource handlers. Every other key will be set by the primary
  • RESTful controller.
  • --->
  • <cfset request.response = true />
  •  
  • <!---
  • This is the error code and message for any errors that occurred
  • during the API request.
  • --->
  • <cfset request.errorCode = 0 />
  • <cfset request.errorText = "" />
  • <cfset request.errorDetail = "" />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Because there are only a small number (relavitely speaking) of
  • generic HTTP status codes, we want to be able to provide more
  • insightful error codes that are relevant to our RESTful API. Each
  • error response should include one of the following values that
  • the 3rd party API developer can use when interacting with and
  • debugging their API responses.
  • --->
  • <cfset request.errorCodes = {} />
  • <cfset request.errorCodes[ "10000" ] = "Unexpected error." />
  • <cfset request.errorCodes[ "11011" ] = "Name is missing." />
  • <cfset request.errorCodes[ "11012" ] = "Name is invalid." />
  • <cfset request.errorCodes[ "11021" ] = "Age is missing." />
  • <cfset request.errorCodes[ "11022" ] = "Age is invalid." />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • While processing the API request, we any number of things could
  • go wrong - both expected and unexpected. As such, we'll wrap all
  • processing in a Try/Catch block so that we can intercept errors
  • and turn them into appropriate RESTful error response values.
  •  
  • NOTE: Custom errors (via CFThrow) must include an [errorCode]
  • attribute so that the error responses can be constructed properly.
  • --->
  • <cftry>
  •  
  • <!---
  • Include the resource processing controller. Imagine that
  • this routes to one of many possible resources (even though
  • this demo only has ONE resource to process).
  • --->
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  • <cfinclude template="process.cfm" />
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!--- Catch bad request errors. --->
  • <cfcatch type="BadRequest">
  •  
  • <!---
  • Set up the error data. Notice that we are using the
  • errorCode in the CFCatch exception to provide more
  • insight into the error.
  • --->
  • <cfset request.statusCode = 400 />
  • <cfset request.statusText = "Bad Request" />
  • <cfset request.errorCode = cfcatch.errorCode />
  • <cfset request.errorText = request.errorCodes[ cfcatch.errorCode ] />
  • <cfset request.errorDetail = cfcatch.message />
  •  
  • </cfcatch>
  •  
  • <!--- Catch any unexpected errors. --->
  • <cfcatch>
  •  
  • <!--- Set up the error data. --->
  • <cfset request.errorCode = 10000 />
  • <cfset request.errorText = request.errorCodes[ "100000" ] />
  • <cfset request.errorDetail = cfcatch.message />
  •  
  • </cfcatch>
  •  
  • </cftry>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Set the response headers. --->
  • <cfheader
  • statuscode="#request.statusCode#"
  • statustext="#request.statusText#"
  • />
  •  
  • <!---
  • Check to see if any errors were raised during the request
  • processing. If so, we need to override the response value with
  • an error response.
  • --->
  • <cfif request.errorCode>
  •  
  • <!--- There was an error. Create an error object. --->
  • <cfset request.response = {} />
  • <cfset request.response[ "statusCode" ] = request.statusCode />
  • <cfset request.response[ "errorCode" ] = request.errorCode />
  • <cfset request.response[ "errorText" ] = request.errorText />
  • <cfset request.response[ "errorDetail" ] = request.errorDetail />
  • <cfset request.response[ "resource" ] = cgi.script_name />
  • <cfset request.response[ "verb" ] = cgi.request_method />
  •  
  • </cfif>
  •  
  • <!--- Serialize the response. --->
  • <cfset serializedResponse = serializeJSON( request.response ) />
  •  
  • <!---
  • Stream response to the client as a JSON body.
  •  
  • NOTE: We are using "text/plain" rather than "application/json"
  • so that we can more easily render the response in the browser
  • for the demo.
  • --->
  • <cfcontent
  • type="text/plain"
  • variable="#toBinary( toBase64( serializedResponse ) )#"
  • />

As you can see, I have a struct, request.errorCodes, that contains the possible errors that will be raised by the API. I then have CFCatch tags that are designed to catch high-level CFThrow errors and translate them into more specific error response data.

The one resource for this API is defined by the process.cfm controller. As you will see in the following code, it halts processing the moment it finds an invalid data point.

Process.cfm - Our API Resource Controller

  • <!---
  • Resource expects:
  •  
  • name: String
  • age: Numeric (0 - 99)
  • --->
  •  
  •  
  • <!--- Check to see if the name is supplied. --->
  • <cfif !structKeyExists( url, "name" )>
  •  
  • <!--- Stop any further processing of this resource. --->
  • <cfthrow
  • type="BadRequest"
  • message="The Name attribute is missing from the request."
  • errorcode="11011"
  • />
  •  
  • </cfif>
  •  
  • <!--- Validate name value. --->
  • <cfif (
  • !len( url.name ) ||
  • (len( url.name ) gt 40) ||
  • reFindNoCase( "[^a-z\s'-]", url.name )
  • )>
  •  
  • <!--- Stop any further processing of this resource. --->
  • <cfthrow
  • type="BadRequest"
  • message="The Name attribute must be between 1 and 40 characters and may only contain letters, spaces, dashes, and apostrophes."
  • errorcode="11012"
  • />
  •  
  • </cfif>
  •  
  • <!--- Check to see if age is supplied. --->
  • <cfif !structKeyExists( url, "age" )>
  •  
  • <!--- Stop any further processing of this resource. --->
  • <cfthrow
  • type="BadRequest"
  • message="The Age attribute is missing from the request."
  • errorcode="11021"
  • />
  •  
  • </cfif>
  •  
  • <!--- Validate age value. --->
  • <cfif (
  • !isNumeric( url.age ) ||
  • (url.age lt 0) ||
  • (url.age gt 99)
  • )>
  •  
  • <!--- Stop any further processing of this resource. --->
  • <cfthrow
  • type="BadRequest"
  • message="The Age attribute must be a number between 0 and 99 (inclusive)."
  • errorcode="11022"
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • If we made it this far, the supplied resource attributes are
  • valid. Now, let's create a fake resource for the demo.
  • --->
  • <cfset request.response = {} />
  • <cfset request.response[ "id" ] = randRange( 1000, 9999 ) />
  • <cfset request.response[ "name" ] = url.name />
  • <cfset request.response[ "age" ] = url.age />

As you can see, each point of validation uses a CFThrow tag with an errorCode attribute that maps to a key in the request.errorCodes collection. In addition to the errorCode attribute, I am also providing more verbose information in the message attribute. I figure this way we can break down the error even further, providing insightful debugging information.

Ultimately, I think the right long-term move is going to be making sure that the 3rd party API consumer can react to errors gracefully using the error codes. I think the additional message data should probably only be used to debug unexpected errors. Maybe. In any case, I really like the idea of being able to go beyond simple HTTP Status Codes; I think feature-specific errors codes are going to provide a huge boost to my API architecture.




Reader Comments

May 25, 2012 at 10:57 AM // reply »
3 Comments

Excellent Ben, I liked the article :)

Just a tip (I think) is a best practice to add a status flag in the response even if all was good:

JSON: {
id: 5820,
name: 'Sarah',
age: 37,
status: 'success'
}

Greetings from Mexico! :)


May 25, 2012 at 11:57 AM // reply »
11,243 Comments

@Joaquin,

Hmm, I could definitely see that. I like the idea of there being some sort of always-accessible value that is consistent. If nothing else, it can make code easier to write for the API consumer.

That said, it might be nicer to have the status code in the Headers, like "X-Error-Code" or something. I say this only because for resources that are returned successfully, it might be strange to mix resource-specific and framework-specific code in the same JSON body.

Good thinking!


May 25, 2012 at 1:50 PM // reply »
63 Comments

@Joaquin ...

Not to nitpick but isn't kv statuscode a flag?


May 25, 2012 at 6:39 PM // reply »
3 Comments

@Ben, @Edward

This is just an idea, some articles of RESTful I read says that...

But definitely I'm agree to mantain the JSON body apart of the API framework... :)

Can you share some RESTful articles?


May 25, 2012 at 7:26 PM // reply »
51 Comments

Thanks Ben,
That makes it much clearer. I must admit I misunderstood what you were trying to say about the api regarding using cfthrow.

I particularly like the idea of custom error codes. I have some code I was working on last night that I wanted to send you before I saw this post that I think you might find interesting.

I might actually try incorporate this error code logic and ill put it up for you to have a look at.


May 29, 2012 at 1:11 PM // reply »
11,243 Comments

@Joaquin,

I recently read the RESTful API Design Rulebook by Mark Masse:

http://www.bennadel.com/blog/2324-REST-API-Design-Rulebook-By-Mark-Masse.htm

Definitely will take a while for me to absorb; and, there were parts of it that I didn't agree with (or thought were overly complex). But, definitely a book that I would recommend. I plan to re-read it after I do some more experimentation with my current MVC (Model-View-Controller) experiment.

@Steven,

I think the approach has some value. Of course, the caveat being that (as you KNOW), I am still getting my feet wet with architecture stuff.

Spent a few days trying to get my MVC stuff up and running. SLOW process - not using a database as a means to 1) work on my computer at home and 2) force myself to not break cohesive concerns by querying data I shouldn't "know" about.


May 29, 2012 at 6:19 PM // reply »
51 Comments

@Ben,
No worries. Honestly its a relief to find out you don't know everything ;-)

Im actually going to try do a short intro presentation on mvc architecture on ugtv or something similar. If you dont mind I'm going to take some of the questions you asked me, plus others from the comments sections here and incorporate them.


May 29, 2012 at 6:26 PM // reply »
11,243 Comments

@Steven,

Ha ha, no problem. I'm a firm believer that if 1 person is asking questions, many more are also wondering, but not saying anything :)


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 23, 2013 at 5:19 AM
Ask Ben: Print Part Of A Web Page With jQuery
How to print also the background color of table cells and table lines ... read »
May 23, 2013 at 3:55 AM
Javascript Array Methods: Unshift(), Shift(), Push(), And Pop()
very interesting and helpful too. ... read »
May 22, 2013 at 5:35 PM
Script Tags, jQuery, And Html(), Text() And Contents()
This is still an issue 2 years later. jQuery is supposed to remediate these cross browser issues, no? I have been unable to find any statement from the jQuery team calling this behavior "by de ... read »
May 22, 2013 at 12:44 PM
Ask Ben: Query Loop Inside CFScript Tags
In cf10, if you call a function that has: local.result = {}; local.result.msg = ""; local.svc = new query(); local.svc.setSQL("SELECT * FROM..."); local.obj = local.svc.exe ... read »
May 22, 2013 at 12:29 PM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben: What version of Java are you using? Also, did you test users.id to see what Java reports as the data type? I wonder if it's not a Java primitive data type, but getting returned as something ... read »
May 22, 2013 at 11:47 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Dana, Awesome - so it looks like this bug was fixed in ColdFusion 10. Thanks so much for double-checking that. ... read »
May 22, 2013 at 11:37 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
When I c&p and run on cf10, I get: Selected User IDs: 1,4 User 1 selected: YES - YES User 2 selected: NO - NO User 3 selected: NO - NO User 4 selected: YES - YES User 5 selected: NO - ... read »
May 22, 2013 at 11:27 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Tom, Good thought, but no dice. Both of these still exhibit the same behavior: users.id[ users.currentRow ] users[ "id" ][ users.currentRow ] It's just something whacky happening with ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools