Skip to content

Instantly share code, notes, and snippets.

@kirkbushell
Created June 11, 2014 15:27
Show Gist options
  • Save kirkbushell/3621f0cc2ec94cc75c6e to your computer and use it in GitHub Desktop.
Save kirkbushell/3621f0cc2ec94cc75c6e to your computer and use it in GitHub Desktop.
Angular JS HTTP response interceptor
/**
* Interceptor
* Implements functionality to catch various requests and fire events when they happen. This is generally to ensure
* that responses from the server are handled in a uniform fashion across the application. Also, by firing events
* it allows to have any number of handlers attach to the response.
*
* @author Kirk Bushell
* @date 28th March 2013
*/
var module = angular.module('core.interceptor', []);
module.service( 'Messages', [ '$filter', function( $filter ) {
var service = {
/**
* Store all the messages here. The default (base) or the custom messages.
*
* @type {Object}
*/
messages: {
base: {
error: {
create: 'Could not create :resource. Please try again.',
read: 'Could not load :resource. Please try again.',
update: 'Could not update :resource. Please try again.',
'delete': 'Could not delete :resource. Please try again.'
},
success: {
create: ':resource created.',
read: 'Loaded :resource successfully.',
update: ':resource saved.',
'delete': ':resource deleted.'
}
},
// Custom messages are stored here.
custom: {}
},
get: function( resource , action , type ) {
resource.split( '-' ).join( ' ' );
resource = $filter( 'ucfirst' )( resource );
var customResource = get( service.messages.custom[ resource.toLowerCase() ] );
if ( customResource != null ) {
var custom = get( customResource[ type ][ action ] );
if ( custom != null ) return custom;
}
// Get the default message, if available, and perform the replacement.
var msg = get( service.messages.base[ type ][ action ] );
if ( msg && msg.length > 0 ) {
msg = msg.split( ':resource' ).join( resource.singularize() );
}
return msg;
},
/**
* Registers a resource and all its custom messages.
*
* @param {String} resource Resource name
* @param {Object} messages An object with create, read, update, delete keys
*
* @return {void}
*/
register: function( resource , messages ) {
service.messages.custom[ resource ] = messages;
}
};
return service;
}]);
module.config(['$httpProvider', function($httpProvider) {
var interceptor = ['$rootScope', '$q', 'Notify', 'Messages', 'Analytics', function($rootScope, $q, Notify, Messages, Analytics ) {
/**
* Parses the resource based on the url that was sent.
*
* @param {object} response Response object
*
* @return {string}
*/
var getResourceFromResponse = function( response ) {
var urlParts = response.config.url.split('?'),
url = urlParts[0].replace(/^\/|\/$/g, ''), // strip first and last slash.
base = $rootScope.config.app.base.replace(/^\/|\/$/g, ''); // strip first and last slash.
// If there's a base, let's strip it out.
if ( base.length ) {
url = url.replace( base , '' );
}
urlParts = url.split('/');
url = urlParts[0];
return url;
}
/**
* Extracts an ID from a URL, if it's available.
*
* @param {string} resource The name of the resource.
* @param {string} url The URL to parse.
*
* @return {mixed} Returns the extracted ID or null.
*/
var getIDFromURL = function( resource , url ) {
var urlParts = url.split( resource ),
possibleID = urlParts[ 1 ],
id;
// If there is nothing, we know we're creating and not updating.
// Therefore, there will be no ID to return.
if ( !possibleID ) return null;
// Remove the first occurance of a slash.
possibleID = possibleID.replace('/', '');
// Check if there are any more slashes, so we know whether we should split
// the possible ID variable or just return it.
if ( possibleID.indexOf('/') === -1 ) {
return isNaN( possibleID ) ? null : possibleID;
}
// Since at this point we know that the url still has a slash in it, we will
// split on that slash and get the first part of the array.
id = possibleID.split('/')[0];
// Now we want to check if the value is a number or not.
return isNaN( id ) ? null : id;
};
/**
* Returns a custom resource action, if it has been supplied in the URL. This is defined
* by a string representation AFTER the resource id. Eg.
*
* /entries/1/submit
*
* @param string resource
* @param string url
* @return mixed string on success, null on failure
*/
var getActionFromUrl = function( resource, url ) {
url = url.replace( config.app.base, '' ).split( '/' );
url.shift();
// Could be dealing with an integer or extra action
if ( url.length > 1 ) {
if ( isNaN( url[ 1 ] ) ) {
// custom action
return url[ 1 ];
}
if ( url.length > 2 && isNaN( url[ 2 ] ) ) {
return url[ 2 ];
}
}
return null;
};
/**
* Based on the data returned from the server whenever there's a validation error
* we will construct a single validation message that is displayed in an alert.
*
* @param {Object} data The data object returned from the server.
*
* @return {String}
*/
var getValidationMessages = function( response ) {
var errors = [];
// Check if the response is empty, meaning there are no validation errors.
if ( getResponseType(response) != 'validation' || $.isEmptyObject( response.data ) || $.isEmptyObject( response.data.errors ) ) return errors;
// Put all the errors from all the fields into 1 array. Basically flatenning the array.
angular.forEach( response.data.errors , function( issues ) {
angular.forEach( issues , function( error ) {
errors.push( error );
});
});
return errors;
}
var getResponseType = function( response ) {
return get( response.headers()[ 'x-response-type' ] );
}
var notificationate = function( response ) {
var method = response.config.method.toLowerCase(),
action = '',
status = response.status,
type = 'Error', // notification type. Error, Success, Info or Warning.
responseType = getResponseType( response ), // custom response type.
message = null,
resource,
possibleAction;
// Ignore GET requests.
if ( method == 'get' && status == 200 ) return;
// Any status 200 responses at this point are for successful operations.
// So we will change the notification type to Success.
if ( status == 200 ) type = 'Success';
// Parse the resource name.
resource = getResourceFromResponse( response );
// Do not display any message for exceptions.
if ( resource == 'exceptions' ) return;
// Let's determine what action is taken based on the request method.
switch ( method ) {
case 'post': action = 'create'; break;
case 'get': action = 'read'; break;
case 'put': action = 'update'; break;
case 'delete': action = 'delete'; break;
}
// Check if there's an ID in the URL whenever we send a post request because the action
// could be update instead of create.
// This is done because ngResource sends a POST request for updates instead of PUT.
if ( method == 'post' ) {
if ( getIDFromURL( resource , response.config.url ) ) {
action = 'update';
}
}
// Set up our action based on whether or not a custom one has been defined in the URL
possibleAction = getActionFromUrl( resource, response.config.url );
if ( possibleAction ) {
action = possibleAction;
}
// Based on the response status.
switch ( status ) {
case 400:
if ( responseType == 'validation' ) {
if ( typeof response.data.message == 'string' ) {
message = response.data.message;
}
}
else {
message = response.data;
}
break;
case 401:
message = 'Your current session has expired. Please log in.';
break;
case 403:
message = 'You do not have sufficient permission to access this resource.';
break;
case 500:
if ( angular.isString( response.data ) && response.data.length ) {
message = response.data;
}
break;
}
if ( !message ) {
message = Messages.get( resource , action , type.toLowerCase() );
}
// Send an update to the validation-errors directive to show/hide validation errors.
$rootScope.$broadcast( 'validation.errors', getValidationMessages( response ) );
if ( message ) {
Notify[type]( message );
// Google Analytics event tracking.
Analytics.trackEvent( resource , type , message );
}
}
/**
* Broadcasts an event that any part of the app can listen to
* and perform custom actions.
*
* @param string name The event name
* @param mixed response The response that comes back from the server
*
* @return void
*/
var broadcast = function( name , response ) {
$rootScope.$broadcast( name , response );
notificationate( response );
}
/**
* Successful response handler.
*
* @param mixed response The response th at comes back from the server
*
* @return mixed
*/
var success = function( response ) {
broadcast( 'app.success' , response );
return response;
}
/**
* Invalid response handler.
*
* @param mixed response The response th at comes back from the server
*
* @return mixed
*/
var error = function( response ) {
// This is the default error event to broadcast.
// It may be overwritten depending on the response status.
var event = 'app.unknown-error';
switch (response.status) {
case 400: event = 'app.error'; break; // Bad requests (validation errors.etc.)
case 401: event = 'app.unauthorised'; break; // Unauthorised, should require login
case 403: event = 'app.forbidden'; break; // Forbidden, user is simply not allowed access
case 500: event = 'app.failure'; break; // Critical error on the server, catch and display
}
broadcast( event , response );
return $q.reject( response );
}
return function(promise) {
return promise.then( success, error );
};
}];
$httpProvider.responseInterceptors.push( interceptor );
}]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment