Skip to main content
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Gareth Sykes
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Gareth Sykes

AngularJS Will Parse JSON Payloads In Non-2xx HTTP Responses

By
Published in , Comments (2)

This is probably an AngularJS feature that you're already using; but, you may not be cognizant of the fact that it's happening. When you make an HTTP request with AngularJS, the request and response goes through a promise chain that applies a series of pre and post transformations. The default transformation, that AngularJS provides, attempts to parse the HTTP response body as JSON. And, it does this regardless of the HTTP status code.

If you dig into the AngularJS source code, you'll see that AngularJS sets up a promise chain, wrapped around the request to the remote server. And, in that request, you'll see that AngularJS pipes the response into a transformation method:

sendReq(config, reqData).then(transformResponse, transformResponse);

Notice that AngularJS is using the same method - transformResponse - as both the resolution handler and the rejection handler. This is because AngularJS will attempt to transform the response in the same way, regardless of whether or not the status code indicates a successful HTTP request. Now, it will still resolve all 2xx level codes and reject all non-2xx level codes; but, it will pass the same deferred value though to both handlers.

To demonstrate this, I set up a small ColdFusion API that returns different HTTP status codes based on the incoming request:

<cfscript>

	param name="url.test" type="string" default="";

	// Set up the default API response settings.
	response = {
		statusCode = 200,
		statusText = "OK",
		data = ""
	};

	// Check to see which status-code we are going to test. Each of the status codes,
	// regardless of an indicated success ( 2xx ) or failure ( 4xx, 5xx ) will return
	// JSON (JavaScript Object Notation) to the client.
	if ( url.test == 200 ) {

		response.data = {
			"id" = 4,
			"name" = "Kim",
			"codeName" = "Awesome Sauce"
		};

	} else if ( url.test == 400 ) {

		response.statusCode = 400;
		response.statusText = "Bad Request";

		response.data = {
			"type" = "App.BadRequest",
			"message" = "You can't do that!",
			"code" = 40343
		};

	} else if ( url.test == 500 ) {

		response.statusCode = 500;
		response.statusText = "Server Error";

		response.data = {
			"type" = "App.ServerError",
			"message" = "Something has gone horribly wrong!",
			"code" = 666666
		};

	} else {

		throw( type = "App.UnexpectedTestCase" );

	}

</cfscript>

<cfheader
	statuscode="#response.statusCode#"
	statustext="#response.statusText#"
	/>

<!--- Reset the output buffers and stream JSON to the client. --->
<cfcontent
	type="application/json"
	variable="#charsetDecode( serializeJson( response.data ), 'utf-8' )#"
	/>

Notice that the incoming query-string parameters, "test", will dicate the status code returned by the server. Also notice that if an unsupported value is passed-in, we'll throw an error. This last case is to demonstrate that a 500-level error may or may not contain valid JSON (JavaScript Object Notation).

Then, I created a small AngularJS app that hits the remote API endpoint and logs out the response:

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		AngularJS Will Parse JSON Payloads In Non-2xx HTTP Responses
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

	<h1>
		AngularJS Will Parse JSON Payloads In Non-2xx HTTP Responses
	</h1>

	<ul>
		<li>
			<a ng-click="testRequest( 200 )">Test 200 OK</a>
		</li>
		<li>
			<a ng-click="testRequest( 400 )">Test 400 Bad Request</a>
		</li>
		<li>
			<a ng-click="testRequest( 500 )">Test 500 Server Error</a>
		</li>
		<li>
			<a ng-click="testRequest( 000 )">Test Unexpected Error</a>
		</li>
	</ul>


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.15.min.js"></script>
	<script type="text/javascript">

		// Create an application module for our demo.
		var app = angular.module( "Demo", [] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I control the root of the application.
		app.controller(
			"AppController",
			function( $scope, $http ) {

				// I make an HTTP request to the server / api, testing the given code.
				$scope.testRequest = function( statusCode ) {

					var promsie = $http({
						method: "get",
						url: "./api/index.cfm",
						params: {
							test: statusCode
						}
					});

					// We are using the same handler for both the resolve and reject
					// handlers. In this case, the rejected response may contain JSON,
					// but it may not.
					promsie.then( handleResponse, handleResponse );


					// I handle the HTTP response.
					function handleResponse( response ) {

						console.info( "HTTP Response [ %s %s ]", response.status, response.statusText );

						// The response.data is the data that has passed through the
						// HTTP promise chain, including the default HTTP response
						// transformation method, which will attempt to deserialize JSON.
						console.log( response.data );

					}

				};

			}
		);

	</script>

</body>
</html>

Notice that I am using the same promise handler for both the resolution and the rejection state. This is because I just want to log the response, regardless of HTTP status code. And, when I run this app and hit each end-point, I get the following output:

AngularJS will parse JSON (JavaScript Object Notation) in non-2xx level HTTP responses.

Notice that AngularJS parsed the JSON response value and passed it through to the promise handlers regardless of whether or not the HTTP response had a 2xx-level status code.

This is awesome; but, we still need to think about the rejection handler. In the demo, we have two different 500-level responses - one that contained JSON and one that contained a error-dump from ColdFusion. This means that, while AngularJS will parse JSON in a non-2xx level response, it doesn't mean that all responses will necessarily contain JSON. As such, you will likely need to normalize your error responses. This might entail wrapping unexpected errors in a client-side object; or, it might entail unwrapping the JSON response (as I did in the error-handler in my post on using the $http service to make AJAX requests).

Like I said above, you probably already make use of this feature in AngularJS; but, it's possible that you've never stopped to think about how this works or what the implications are. Personally, I think this is awesome and makes life just a little bit easier.

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

Reader Comments

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