Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with:

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

By Ben Nadel on
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

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! :)

Reply to this Comment

@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!

Reply to this Comment

@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?

Reply to this Comment

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.

Reply to this Comment

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

Reply to this Comment

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

Reply to this Comment

@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 :)

Reply to this Comment

Here's a very important side-effect when using cfheader's statusText attribute: caching and/or proxy/firewall servers can obliterate this value.

I did some very clear experiments today, and visibly saw that our smart caching network (NTT SCD, so Akamai perhaps as well?) rewrote the status text of our 500 error to the default "Internal Server Error" string. I could hit the server with two different domains, one which forwards through the caching servers and one which does not, and watch the two different strings get returned.

Trying the non-caching domain from inside our company's firewall, I again saw it rewritten back to "Internal Server Error".

So don't count on the statustext being available to e.g. ajax calls for special error trapping routines. Use a custom header instead.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.