AngularJS $http Interceptors: Centralize HTTP Logic in Legacy Apps
AngularJS $http interceptors let you centralize authentication headers, error handling, and logging across legacy 1.7.x apps without touching every service. Covers registration, ordering, lazy-injection patterns, and verification steps.
11 Aug 2026, 10:44 UTC

The Problem: Scattered HTTP Concerns
In AngularJS 1.x applications that have grown beyond a few screens, every $http call often carries the same boilerplate: attaching an Authorization header, logging the request URL, or handling a 401 by redirecting to login. If the API changes its token format or the security policy shifts, you are looking at a grep-and-replace across dozens of files. This is a classic cross-cutting concern, and AngularJS provides a built-in mechanism to solve it: $http interceptors.
How $http Interceptors Work
An interceptor is a factory function registered with $httpProvider.interceptors.push() inside a module config block. It returns an object with up to four optional methods:
- request(config) - runs before the request fires. Modify config, add headers, inject CSRF tokens.
- requestError(rejection) - handles synchronous failures during the request phase (rare).
- response(response) - transforms successful responses. The last registered interceptor processes these first.
- responseError(rejection) - catches failed requests. Use for global toasts, retry logic, or 401 redirects to login.
Each method receives a config object or a promise and must return a config, response, or promise. Returning a promise enables asynchronous work, such as refreshing an expired token before retrying the original request.
Registration Order and Execution Symmetry
The order in which you push interceptors matters. Request interceptors execute in registration order (first registered runs first). Response interceptors execute in reverse order (last registered runs first). This symmetry lets you layer concerns without coupling:
angular.module('app') .config(['$httpProvider', function($httpProvider) { $httpProvider.interceptors.push('loggingInterceptor'); // 1st on request, last on response $httpProvider.interceptors.push('authInterceptor'); // 2nd on request, 2nd on response $httpProvider.interceptors.push('cachingInterceptor'); // 3rd on request, 1st on response }]);In this setup, logging sees the raw request first and the final response last. Caching can short-circuit the response before auth even touches it. If you need a different order, adjust the push sequence accordingly.
Worked Example: Auth Header with Lazy Service Injection
A frequent pitfall occurs when an interceptor needs AuthService, but AuthService itself depends on $http. Direct injection triggers a circular dependency error. The idiomatic fix is lazy lookup via the $injector service:
angular.module('app') .factory('authInterceptor', ['$q', '$injector', function($q, $injector) { return { request: function(config) { // Skip template loads or explicitly opt-out endpoints if (config.skipAuth) return config; var AuthService = $injector.get('AuthService'); var token = AuthService.getToken(); if (token) { config.headers = config.headers || {}; config.headers.Authorization = 'Bearer ' + token; } return config; }, responseError: function(rejection) { if (rejection.status === 401) { var AuthService = $injector.get('AuthService'); AuthService.logout(); var $state = $injector.get('$state'); $state.go('login'); } return $q.reject(rejection); // propagate to caller .catch() }; }]);Register it in your config block with $httpProvider.interceptors.push('authInterceptor'). The skipAuth flag lets unauthenticated endpoints or template loads opt out without modifying the interceptor logic.
Diagnostic Check: Verify Registration Order
To confirm your interceptor pipeline is set up as intended, run this in the browser console or a dedicated script tag after your app boots:
angular.module('app').config(['$httpProvider', function($hp) { console.log('Interceptor order:', $hp.interceptors); }]);Expected output: an array listing your interceptors in registration order. For request execution, the first element runs first; for response execution, the last element runs first. If the order seems off, adjust the push calls.
Risk note: Interceptors apply to all $http calls, including $templateRequest and $resource actions. Add config.skipAuth = true on specific calls to opt out selectively, or risk unexpected header injection on template fetches.
Limitations and a Practical Migration Nudge
AngularJS 1.x reached end-of-life in December 2021. No security patches are issued, and newer browser features may not be fully supported. Production apps should assess risk and plan a migration path to Angular (2+) or another modern framework.
Within the constraints of a legacy app, interceptors remain the idiomatic way to handle cross-cutting HTTP logic without polluting controllers. Just remember: interceptors operate at the $http service level and cannot directly access component-scoped $scope or controller instances. Pass state via config.params, headers, or shared services.
If your application uses business-logic validation that returns 422-style field errors, over-relying on a global responseError interceptor can obscure granular handling. Prefer letting callers .catch() their own promises for UX-specific behavior.
Quick Verification Checklist
- Create a minimal AngularJS 1.7.x app (e.g., npm install angular@1.7.9)
- Register an interceptor that logs config.url in the request hook and response.status in the response hook
- Trigger $http.get('/api/test') and open the console; you should see the request log before the response log, matching the registration order
- Test error chaining: return $q.reject(rejection) in responseError to propagate the failure, or return a new promise to recover; verify the downstream .catch() receives the expected value