Created
June 11, 2014 15:27
-
-
Save kirkbushell/3621f0cc2ec94cc75c6e to your computer and use it in GitHub Desktop.
Angular JS HTTP response interceptor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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