Diagnosing 401 Unauthorized Errors in Angular HttpClient Despite a Valid Bearer Token
A step‑by‑step diagnostic guide for fixing 401 Unauthorized responses in Angular HttpClient when a bearer token appears to be present.
09 Feb 2026, 18:32 UTC

Recognizable condition
Your Angular application makes HTTP calls using HttpClient. The requests include a bearer token that you know is valid (e.g., you can decode it and see a future expiry), yet the server consistently returns 401 Unauthorized. The error appears in the console as an HttpErrorResponse with status 401, and the Network tab shows the request was sent without an Authorization header or with an incorrect value.
Cause / Diagnostic table
| Observed symptom | Likely cause | Notes |
|---|---|---|
| No Authorization header in the request | Auth interceptor runs after the request is already sent, or its next() is never called | Check interceptor order and ensure each interceptor forwards the request. |
| Authorization header present but contains an outdated token | A downstream interceptor overwrites the header set by the auth interceptor | Look for interceptors that modify headers without preserving existing ones. |
| Request fails with 401 even though token is refreshed | Error‑handling interceptor throws or returns an error without calling next(), aborting the chain | Ensure catchError either retries after token refresh or re‑throws correctly. |
Ordered checks
- Inspect token storage
- Open the browser console and verify that the token retrieved from your storage service (e.g.,
localStorage.getItem('access_token')) matches the expected JWT. - If the token is missing or malformed, fix the storage/retrieval logic before proceeding.
- Open the browser console and verify that the token retrieved from your storage service (e.g.,
- Verify interceptor chain via Angular DevTools
- Install Angular DevTools (if not already).
- Reload the app, trigger a request, and open the DevTools “Profiler” tab. Look for the “HttpInterceptor” events to see the order in which they fire.
- Confirm that the interceptor responsible for adding the
Authorizationheader appears before any error‑handling interceptor.
- Confirm the Authorization header is set before each request
- Add a temporary
console.loginside the auth interceptor:
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { const token = this.tokenService.getToken(); console.log('Auth interceptor – token:', token ?? 'none'); const cloned = req.clone({ setHeaders: { Authorization: token ? `Bearer ${token}` : '' } }); return next.handle(cloned); } - Add a temporary
- Open the Network tab, select the request, and check the “Headers” section. The
Authorizationvalue should match the logged token. - Check for header‑overwriting interceptors
- Search your codebase for any interceptor that calls
req.clone({ setHeaders: { Authorization: ... } })without first reading the existing header. - If found, modify it to preserve the existing value:
// Bad: overwrites whatever was there req.clone({ setHeaders: { Authorization: `Bearer ${newToken}` } }); // Good: preserves existing header if present const authHeader = req.headers.get('Authorization'); req.clone({ setHeaders: { Authorization: authHeader ? authHeader : `Bearer ${newToken}` } }); - Search your codebase for any interceptor that calls
- Ensure error interceptor calls next() after handling
- In your
catchErrorinterceptor, after attempting a token refresh, either retry the original request or re‑throw the error:
return next.handle(req).pipe( catchError(async (err) => { if (err.status === 401) { const refreshed = await this.authService.refreshToken(); if (refreshed) { const retryReq = req.clone({ setHeaders: { Authorization: `Bearer ${refreshed}` } }); return next.handle(retryReq); // retry after refresh } } // If refresh fails or not a 401, propagate the error return throwError(() => err); }) ); - In your
- Verify that the interceptor does not simply return
throwError(() => err)without callingnext()for the retry case.
Fixes tied to findings
- Reorder interceptors – In
AppModule(or wherever you provideHTTP_INTERCEPTORS), place the token‑adding interceptor before any logging or error interceptors:
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthTokenInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorHandlerInterceptor, multi: true }
]
- Preserve existing headers – Adjust any interceptor that modifies headers to read the current value first, as shown in the diagnostic step.
- Add a refresh‑and‑retry pattern – Implement the
catchErrorlogic that attempts to obtain a fresh token and retries the request, ensuring the chain continues vianext.handle.
Escalation criteria
- If the token service consistently returns a malformed JWT (e.g., missing
.segments or incorrect base64), involve the backend team to verify the token issuance process. - If reordering interceptors breaks functionality of a third‑party library (e.g., an HTTP caching interceptor that expects to be first), isolate each interceptor in a standalone test bed using
HttpTestingControllerto confirm its individual behavior before deciding on a compromise order. - When the refresh token endpoint itself returns 401, the problem lies outside the Angular frontend; raise the issue with the authentication service owners.
Verification
- Console logging – After applying the fixes, reload the app and watch the console for the auth interceptor log. Confirm the token value is present and matches the storage.
- Network tab inspection – Perform a request that previously failed. In Chrome DevTools → Network → (request) → Headers, verify that the
Authorizationheader contains the expectedBearer <token>value. - Unit test with HttpTestingController – Write a test that injects the interceptor chain, makes a request, and asserts that the request passed to the backend mock has the correct header:
it('should attach a valid token to outgoing requests', () => {
const mockToken = 'valid-jwt-token';
tokenService.getToken.and.returnValue(mockToken);
httpClient.get('/api/data').subscribe();
const req = httpTestingController.expectOne('/api/data');
expect(req.request.headers.has('Authorization')).toBeTrue();
expect(req.request.headers.get('Authorization')).toBe(`Bearer ${mockToken}`);
req.flush({});
});
If the test passes and the Network tab shows the correct header, the 401 issue is resolved for the tested scenario.
Limitations
- The diagnostic steps assume you have access to modify the interceptor providers. If the application uses a third‑party module that provides its own interceptors, you may need to use
InjectionTokenorforwardRefto adjust ordering. - Token refresh logic varies by backend implementation; the example uses a simple
refreshToken()method that returns a new access token string. Adapt the call to match your service’s API. - Console logging added for verification should be removed or guarded by a feature flag in production to avoid leaking sensitive token information.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.