Skip to main content
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Nick Carter and Jessica Kennedy
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Nick Carter Jessica Kennedy

Using Appropriate Status Codes With Each API Response

By
Published in , Comments (29)

For a long time, I have thought about API request failures as falling into just two distinct categories: failure to communicate (ie. the server was down) or bad data (ie. invalid parameters). Failures to communicate with the server were out of my hands; as such, there was nothing I could do with those from a server standpoint. Requests with bad data, on the other hand, were certainly something within my domain of control and, happened to be something that I had strong feelings about.

Much of what I believe about API responses comes from my experience with SOAP-based web services. If you look at the SOAP request / response life cycle, you'll notice that SOAP responses always return a 200 status code, even when the request is invalid. Granted, the response might contain a SOAP fault (error) structure; but, from an HTTP standpoint, the request was successful.

I took this SOAP approach and extended it to my non-SOAP APIs. Typically, in my API architecture, the responses returned from the server always contain a 200 status code. Even if the request happens to be invalid, any error information would be contained in the body of a 200 response. This has been working well for me; but a few months ago, in a post about handling AJAX errors with jQuery, Simon Gaeremynck suggested that I use more appropriate status codes to describe the API response.

When using a variety of status codes in jQuery, only 200 responses will be handled by the success callback function; all other responses - 400, 404, 500, etc. - will be handled by the error callback function. I have to say, there is definitely something very delicious about having the success callback function handle only successful requests; I think it would keep the success work flow much cleaner. Definitely, this is something worth exploring; and, while it may have taken me a few months to get around to it, I think I like what I am seeing.

To test this, I set up a simple ColdFusion API page. For demo purposes, the API will do nothing but return a Girl object with an ID of 4. If the ID is not passed in, I will return a Bad Request response (status code 400). If the ID is passed in, but is not 4, I'll return a Not Found response (status code 404). And, if there is an unexpected error, I'll return an Internal Server Error (status code 500).

api.cfm

<!---
	Set up a default response object. This is not the object that
	we are going to send back to the client - it is just an
	internal value object that we will use to prepare the response.
--->
<cfset apiResponse = {
	statusCode = "200",
	statusText = "OK",
	data = ""
	} />


<!---
	We're going to wrap the entire processing algorithm in a try /
	catch block so that we can catch any request and processing
	errors and return the appropriate response.
--->
<cftry>


	<!--- Try to param the URL varaibles. --->
	<cftry>

		<!--- Param the URL paramters for the request. --->
		<cfparam name="url.id" type="numeric" />

		<!--- Catch any malformed request errors. --->
		<cfcatch>

			<!--- Throw a local error for malformed request. --->
			<cfthrow
				type="BadRequest"
				message="The required parameter [ID] was not provided, or was not a valid numeric value."
				/>

		</cfcatch>

	</cftry>


	<!--- ------------------------------------------------- --->
	<!--- ------------------------------------------------- --->


	<!---
		Check to see if the given ID is a valid ID. For demo
		purposes, we are going to only allow the ID - 4.
	--->
	<cfif (url.id neq 4)>

		<!---
			The given ID does not correspond to a valid value in
			our "database". Throw an item not found error.
		--->
		<cfthrow
			type="NotFound"
			message="The requested record with ID [#url.id#] could not be found."
			/>

	</cfif>


	<!--- ------------------------------------------------- --->
	<!--- ------------------------------------------------- --->


	<!---
		If we made it this far, the request is in a valid format
		and the parameters are accurate. Let's set up the response
		values (for our demo, we are going to pretend that we are
		pulling out of a database).
	--->
	<cfset apiResponse.data = {
		id = url.id,
		name = "Joanna"
		} />


	<!--- ------------------------------------------------- --->
	<!--- ------------------------------------------------- --->


	<!---
		If there was a problem here, the request itself was
		malformed (meaning, either the require parameters were
		not sent, or they were not valid). This should result in
		a status code of "400 Bad Request".
	--->
	<cfcatch type="BadRequest">

		<!---
			Since this was a malformed request, let's set the
			status code to be a 400.
		--->
		<cfset apiResponse.statusCode = "400" />
		<cfset apiResponse.statusText = "Bad Request" />

		<!--- Set the data to be an array of error message. --->
		<cfset apiResponse.data = [ cfcatch.message ] />

	</cfcatch>


	<!---
		If the record could not be found, the parameters were
		correctly formatted, but were not accurate. This should
		result in a status code of "404 Not Found".
	--->
	<cfcatch type="NotFound">

		<!---
			Since this request did not point to a valid record,
			let's set the status code to be a 404.
		--->
		<cfset apiResponse.statusCode = "404" />
		<cfset apiResponse.statusText = "Not Found" />

		<!--- Set the data to be an array of error message. --->
		<cfset apiResponse.data = [ cfcatch.message ] />

	</cfcatch>


	<!---
		If we are catching an error here, it means that an
		unexpected exception has been raised. This should result
		in a status code of "500 Internal Server Error".
	--->
	<cfcatch>

		<!---
			Since something unexpected went wrong, let's set the
			status code to be a 500.
		--->
		<cfset apiResponse.statusCode = "500" />
		<cfset apiResponse.statusText = "Internal Server Error" />

		<!--- Set the data to be an array of error message. --->
		<cfset apiResponse.data = [ cfcatch.message ] />

	</cfcatch>


</cftry>


<!---
	At this point, we have processed the request (either
	successfully or unsuccessfuly); we now have to return a
	value to the client. First, let's serialize the response.
--->
<cfset responseString = serializeJSON( apiResponse.data ) />

<!--- Convert the response to binary for streaming. --->
<cfset responseBinary = toBinary( toBase64( responseString ) ) />


<!--- Set the status code and text based on the processing. --->
<cfheader
	statuscode="#apiResponse.statusCode#"
	statustext="#apiResponse.statusText#"
	/>

<!---
	Set the content length so the client knows how much data
	to expect back.
--->
<cfheader
	name="content-length"
	value="#arrayLen( responseBinary )#"
	/>

<!--- Stream the content back to the client. --->
<cfcontent
	type="application/x-json"
	variable="#responseBinary#"
	/>

As you can see, I create an initial response data object, apiResponse. This is not the object that I end up streaming back to the client; rather, it is just an object that I use to help define my API response. Ultimately, I am only returning "data" with my response - I use the status code and the status text to define the response headers. To determine those response headers, I am simply using a local error handling work flow to throw and catch errors as needed.

With that API in place, I then set up a simple jQuery test page that would make various requests to the API with a variety of data values. Each data value should result in a different type of response (ie. 200, 400, 404).

<!DOCTYPE HTML>
<html>
<head>
	<title>Using Appropriate API Status Codes</title>
	<style type="text/css">

		#output {
			border: 1px solid #999999 ;
			padding: 10px 10px 10px 10px ;
			}

		#output p {
			margin: 3px 0px 3px 0px ;
			}

	</style>
	<script type="text/javascript" src="jquery-1.4.2.js"></script>
	<script type="text/javascript">

		// When the DOM is ready, initialize the scripts.
		jQuery(function( $ ){

			// Get the link references.
			var badRequestLink = $( "a[ rel = '400' ]" );
			var inaccurateRequestLink = $( "a[ rel = '404' ]" );
			var goodRequestLink = $( "a[ rel = '200' ]" );

			// Get our output reference.
			var output = $( "#output" );


			// This is the function that will handle all of the
			// AJAX requests.
			var makeAPIRequest = function( data ){

				// Make the API call with the given data.
				$.ajax({
					type: "get",
					url: "./api.cfm",
					data: data,
					dataType: "json",

					// This method will handle 200 responses only!
					success: function( response ){

						// Show the successful response.
						showSuccess( response );

					},

					// This method will handle all non-200
					// reponses. This will include 400, 404, and
					// 500 status codes.
					error: function( xhr, errorType ){

						// Check to see if the type of error is
						// "error". If so, then it's an error
						// thrown by our server (if it is a
						// "timeout", then the error is in the
						// commuication itself).
						//
						// NOTE: Because this is an error, jQuery
						// did NOT parse the JSON response; as
						// such, we have to do that manually.
						if (errorType == "error"){

							// Show the error.
							showError(
								xhr.status,
								xhr.statusText,
								$.parseJSON( xhr.responseText )
							);

						}
					}
				});

			};


			// I show error responses.
			var showError = function( statusCode, statusText, errors ){
				output.html(
					("<p>StatusCode: " + statusCode + "</p>") +
					("<p>StatusText: " + statusText + "</p>") +
					("<p>Errors: " + errors.join( ", " ) + "</p>")
				);
			};


			// I show success responses.
			var showSuccess = function( girl ){
				output.html(
					("<p>ID: " + girl.ID + "</p>") +
					("<p>Name: " + girl.NAME + "</p>")
				);
			};


			// Bind the bad request
			badRequestLink.click(
				function( event ){
					// Prevent the default action (location).
					event.preventDefault();

					// Make the API call without any data.
					makeAPIRequest( {} );
				}
			);


			// Bind the inaccurate request
			inaccurateRequestLink.click(
				function( event ){
					// Prevent the default action (location).
					event.preventDefault();

					// Make the API call with inaccurate data.
					makeAPIRequest( { id: 1 } );
				}
			);


			// Bind the good request
			goodRequestLink.click(
				function( event ){
					// Prevent the default action (location).
					event.preventDefault();

					// Make the API call with good data.
					makeAPIRequest( { id: 4 } );
				}
			);

		});

	</script>
</head>
<body>

	<h1>
		Using Appropriate API Status Codes
	</h1>

	<p>
		Make a <a href="#" rel="400">Bad Request</a>.
	</p>

	<p>
		Make an <a href="#" rel="404">Inaccurate Request</a>.
	</p>

	<p>
		Make a <a href="#" rel="200">Good Request</a>.
	</p>

	<div id="output">
		<em>No response yet.</em>
	</div>

</body>
</html>

As you can see, each of the three links - Bad Request, Inaccurate Request, Good Request - triggers an AJAX request to the API. These AJAX requests define both a success and an error callback handler. The success callback handles the 200 responses only. The error callback, on the other hand, will handle our 400 and 404 responses (and 500, which I am not demoing). Unfortunately, jQuery does not pass any response data to the error callback handler. As such, we need to manually gather the responseText from the XMLHTTPRequest object and use jQuery to parse it into valid Javascript objects for us.

As someone who was adamantly against this type of approach in the past, I have to admit that there is something about this that I find very appealing. It uses the callback handlers in a way that feels much more intentful; in fact, the whole architecture, both server-side and client-side, feels much more intentful. I think Simon was right - this is a cleaner approach to handling errors in AJAX. Thanks Simon!

Want to use code from this post? Check out the license.

Reader Comments

6 Comments

Good to see you came to the light side ;)

As far as jQuery is concerned, I agree that it isn't very consistent, but if you keep it in mind it's not a big concern (IMHO).

I also find that an API gets much cleaner this way.

Glad to help,

Simon

1 Comments

Very nice demo! The only thing is the returned message by the server is never .. user-friendly. It's why I never use the server message and I change it for a custom message.

Nice job!

15,811 Comments

@Simon,

Thanks for the help :) I agree - the API does feel cleaner this way; and, when something has more conscious intent behind it, I typically feel it to be the more appropriate approach.

Also, I just linked your full name - your original comments didn't have your URL.

2 Comments

Use this http://www.mediamatic.nl/download/3241 HTTP map to articulate what you want to say.

Very comprehensive :)

ps. ... and start documenting corMVC it looks awesome.

15,811 Comments

@Stephan,

Agreed! That's why I didn't rely on the status text; rather, I passed back JSON data that I had to manually parse into a Javascript object. This way, even with something like a 400 (Bad Request), I could still theoretically return an array of errors for something like form validation.

30 Comments

I've been using the Powernap framework (powernap.riaforge.com) to build RESTful APIs. Makes it very easy to return http status and custom content in the API methods, as well as making it easy to map URIs to method calls. Definitely worth checking out.

15,811 Comments

@dotnetCarpenter,

I'll definitely check that out... and I'll start working on doing something more with corMVC :)

@Ryan,

Thanks, I'll check that out as well.

@Marc,

Any logging, you'll have to do manually. Since no error actually bubbles up to the application - all catching is done locally - there is no exception from the application view point. If you need to be logging things like invalid API calls, that just needs to be part of your business logic (I assume).

Probably, you'll want to create a more CFC-based API that can have core methods for logging and error handling.

20 Comments

Hi Ben,

I guess what I was asking was, do these status codes appear in the web server logs because a 500 error message indicates "internal server error", and if you're throwing that for *application* purposes, it could be difficult to differentiate between app-generated 500's and actual web server problems...

Marc

15,811 Comments

@Marc,

I'm not too learned in server logs; but, as far as I know, only uncaught errors are logged. Since these are being caught as part of the API work flow within the application, I don't believe the server will log them. But that is my hypothesis - not fact.

10 Comments

Ben, first nice post.

I have done the same thing for years, but I actually never thought about returning status codes. I would a struct method that would define ok or failed, then it would be upto the client to then decide from there what to do.

I might have a rethink, as this does seem a bit easier than trying to pick a number and have that define what the problem is.

113 Comments

@Marc,

You should never *generate* 500's in your application on purpose. Your application should always generate 200's, 300's, or 400's. If your application fails in a spectacular way, and cannot recover from failure, then *your appserver (CF) or webserver (Apache)* will respond with a 500.

200's: the request was clean and the desired output is contained within the current HTTP response.
300's: the request was clean and the desired output is *not* contained within the current HTTP response, but is at some other location.
400's: the request was erroneous and could not be completed. Reasons include: bad input data, bad input format, not authenticated, not authorized, etc.
500's: the server screwed up royally. There is no reason for your application to generate this. If you are writing a framework for hosting applications, and an application written for the framework screws up royally, then the framework might return this status. Otherwise, CF or Apache might return this status if there was an unhandled exception, if CF was itself buggy, etc. If your application is generating 500's on purpose, what that means to me is that you need to back to your application and rewrite it so that it starts handling errors in a sane fashion - e.g., respond instead with a 400 if the input data was bad, and log the error behind the scenes.

Cheers,
Justice

15,811 Comments

@Justice,

That's a really good point. In my code, I basically assumed that the default catch would return a 500 since I had no idea what might be throwing that particular error. But, the biggest problem is that if my app does return a 500 error explicitly AND the server might return a 500 error on its own (critical error), then the client needs to understand potentially two different flavors of 500 error... which is not good.

Point well taken, thanks.

20 Comments

@Justice,

I think you actually made my point for me. It's exactly what I was trying to say/thinking of saying.

The more I thought about this whole idea, the less I liked it. A web server throws errors related to serving up web files. It doesn't feel right (to me - this is strictly opinion of course!!) to use one layer's error handling (the web service) in another layer's application (the web application server, ColdFusion.

My question was basically the same -- if your app is throwing web-server-like errors (especially 500!!), and they show up in the web server logs, how will you know the difference between a problem with your web server and a (handled) error by your app?

I don't have a complete grip on the whole concept here and my opinion could be (very easily) swayed, but the 500 caught my eye first.

113 Comments

@Ben, @Marc,

Generally speaking, if you catch an exception and recover from it and handle it in some fashion, that typically means the server did not experience a catastrophic error. It means the user did something stupid, tricky, or otherwise not allowed for some reason or other. The HTTP spec dictates that when the user is at fault, the response status should be in the 400's.

But, to take your scenarios, if you have a global catch which logs an error and rethrows or which logs an error and displays a nifty server-error page, you actually should respond with a 500. That falls under the "framework" case that I outlined above. In a global catch, it's not entirely clear that the web application, or at least this particular request/response, "recovered" from the error well enough to continue processing. Here, the server is actually at fault, not the user. And when the server is at fault, the HTTP spec says to respond with a 500's status.

You can certainly make a distinction between exceptions caught in a global catch and uncaught exceptions by setting an X-Header in your response. An X-Header is any header that begins with "X-". The HTTP spec says that the application is permitted to set any such header in any way it feels like. If you catch an exception in a global catch, feel free to set a header like <cfheader name="X-Exception-Interception" value="app-global-catch">. To HTTP, it is meaningless - but you can still see the X-Header in Firebug and you can still access it with jQuery.

Cheers,
Justice

15,811 Comments

@Justice,

That's interesting about the "X-" headers; I had not heard that before (my understanding of headers is limited).

@Marc, @Justice,

Just to get on the same page about some of the terms we are using, when we set a Header value, we're not actually throwing an exception. Granted, in my example, I am throwing an error to catch an error; but, the simple act of setting a header is *not* the same as raising an exception.

I can agree with you guys that using a 500 is probably not appropriate as @Justice says, since it's not a user-initiated problem. But, I can't see any issues with returning 400's errors. Also, keep in mind that I am never throwing a 400's error - I'm throwing custom errors that are being trapped and then later translated into header values.

15,811 Comments

@Andrew,

Yeah, exactly, I used to do it that way as well - all of my API responses had:

- Success (true / false)
- Errors (arary)
- Data (anything)

... and then the client had to figure out what to do based on the Sucecss / Errors properties.

But, there's something I really like about this status-code based approach since it seems more inline with what is actually going on from the client's point of view.

11 Comments

@Justice

This is exactly how things should be handled, IMO! Within most APIs there are errors and there are exceptions. Exceptions are should be expected to be caught within the API itself and handled for, when possible. Errors on the other hand "bubble up" and, in the case of web facing APIs, change status codes of the response.

In our particular case, we add an additional layer to web facing APIs which are "API Managers". This is what third parties interact with and what changes the headers/sends errors (dumbs things down for third party developers and implements one or more interfaces). Based upon the error caught, the manager changes the response status/headers and also send information about the issue via XML/JSON.

If you use jQuery's ajax function rather than post/get you can really do some great error catching.

15,811 Comments

@Rocky,

So, 500 error aside (as I think we are all agreeing that this should not be programmatically generated based on user input), are we basically agreeing? It sounds like you are using your API managers to send back a variety of 400 errors based on the user input, which is what I was exploring?

11 Comments

@Ben

Yep, we look a lot at the available http status codes to try and find the best match to what the error is. Between the status code, headers, and/or sending back what the error was via json or xml, it works out well for us.

It is certainly more work on both the API and client side but it's really slick once it's done.

Our server side application stack looks something like:

API Library

System level APIs: Application agnostic. Much like the java package in Java or system namespace in .Net. We moved away from UDFs and custom tags, instead we use these system API libraries we have developed that are shared across many applications. For instance, we have a text datatype that, once initialized, can perform many UDF type functions via methods. Ex: system.security.Cryptography or system.datatype.simple.Text

Application level APIs: Application specific APIs which utilize the system level APIs. This is where the heavy lifting and core business rules are housed. Ex: us.co.k12.thompson.sis.user.User or us.co.k12.thompson.sis.user.UserFactory

Remote API Managers: When we want to expose functionality within the application level APIs, we add another layer to simplify things for third-parties, convert errors that would normally be handled in our Web Framework to what I was speak to above.

Web Applications

Core Web Framework: Custom developed MVC framework that provides basic security, variables, caching, and other core functionality/structure. The framework is based on serving "files", not just web pages. It can just as easily deliver jpg, pdf, xls, as it would xhtml. None of the code is exposed directly to the web, just a single cfm file that reroutes the request to the handlers (for extra security). Again, uses 404 handling to mimic any file request that was made that wasn't handled by the web server. The framework utilizes the system level APIs and does some caching of the APIs for performance (most of them can be cached in the application scope).

Request handlers (i.e. webpages in most cases): What most people code when they develop CF. These utilize the system and application level APIs.

11 Comments

I forgot to mention that using this application stack allows me to separate as much of the core/business logic into the API Library which leaves the web applications just to handle presentation layer logic as much as possible. It's also been helpful in moving from one delivery method to another. Since the core logic is tied up in the APIs, I can deliver an application functionality to mobile users, standard web users, even third parties mush easier. Just change the presentation layer stuff and you're all set! :)

Boy, I am way off topic. Sorry about that!

15,811 Comments

@Rocky,

No worries re: going off topic. Sounds like a very interesting architecture you've got going on over there. From everything that people tell me, the more API-based your architecture gets, the more scalable and maintainable your application becomes. Hopefully, as I start to get more and more into OOP, that will happen naturally.

6 Comments

We have a similar setup as Rocky.

We have a backend which only exposes REST services.
It doesn't output any html, js, jsp, .. . (We are capable of running jsp, groovy, jsf, ruby, jython, etc.. but we really choose not to).

We also have a frontend which exists solely out of html/css and js.
Whenever something needs to happen that requires a call to the "database" it goes trough a REST api.

For example:
Take a blog post with some comments, and a user want's to comment on it.

All the frontend related code and markup (ie HTML, CSS, images and JavaScript) are served straight from Apache HTTPD (this is extremely fast).
When the HTML is loaded we execute some JS that retrieves all the "data" that needs to be filled in on the page from a REST service. This will be the blog post + comments.
The user types in his name, email, website and comment and clicks 'Post Comment'. This does an AJAX call to another REST service which adds the comment in the db. If the call was succesfull, we add the comment to the DOM of the page via JS.

A nice benefit of this, is that all these actions only require a single page load.

If we would want to run this on a mobile device we don't have to change anything on our backend since it is exposed via REST.

20 Comments

Simon,

If you're serving up what is essentially a blank page "template", and then getting the actual content for that page using javascript, how on earth are you getting the search engines to see your content? Or was that not a priority for that particular project?

Overall the REST API for EVERYTHING idea is great, especially for a web "app" that doesn't care about SEO... but for a public website, it seems that could be a limiting factor in SEO and indexing.

6 Comments

@Marc,

For our project it isn't really a requirement, since most (if not all) of our resources are private and related to the logged in user.

Anyway, IIRC google does execute javascript before indexing the page.
A good example (for us at least ;)) might be paging.
Imagine you have a blogpost which has ~500 comments. We show the first/last 50 and then load the following 50 via AJAX. No page reload.
It is up to the implementation to leave a URL that can later be found again.
Something along the lines of
example.com/blog/Using-Appropriate-Status-Codes-With-Each-API-Response.htm#50
Where the #50 resembles the starting point to display comments.

It's not really a clean way, but it is a way to place a URL in the navigation bar of the browser that the client can reliably copy and share.

15,811 Comments

@Simon,

Sounds awesome; that's the kind of architecture that I'm trying to learn more about. My recent series of "FLEX on jQuery" is meant to do just that. I am trying to learn more about FLEX so I can create richer, thicker clients that rely more on APIs rather than old-school request/response life cycles.

1 Comments

hello ben,
i have implemented above code in project but i m getting error "There was a problem with the API" can you help me

1 Comments

Thanks for this great post.

Note that this comment is not technically correct:

"When using a variety of status codes in jQuery, only 200 responses will be handled by the success callback function;"

jQuery considers a 2XX status or 304 status to be successful.

If you search for this string:
"if ( status >= 200 && status < 300 || status === 304 )"

in jQuery's source code:
http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js

you'll see how jQuery determines what's successful. This is important, as many APIs return 201 for newly created resources, and this should be considered a success.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel