From harness-claude
Intercept HTTP requests and responses in Angular using HttpInterceptorFn for auth headers, retry logic, loading states, and centralized error handling.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Intercept HTTP requests and responses with HttpInterceptorFn for auth headers, retry logic, loading state, and centralized error handling
Implements signal-based HTTP data fetching in Angular v20+ using httpResource(), resource(), and HttpClient for API calls, loading states, error handling, and Observable-to-signal conversion.
Implements NestJS Interceptors to transform responses, log execution times, add timeouts, cache results, and handle errors across routes.
Implements NestJS guards and interceptors for authentication, authorization, logging, and request/response transformation. Covers CanActivate, ExecutionContext, and JWT patterns for cross-cutting concerns.
Share bugs, ideas, or general feedback.
Intercept HTTP requests and responses with HttpInterceptorFn for auth headers, retry logic, loading state, and centralized error handling
HttpInterceptorFn (functional, Angular 15+) rather than class-based HttpInterceptor. Functional interceptors use inject() and compose cleanly as arrays.provideHttpClient(withInterceptors([authInterceptor, loggingInterceptor])) in your app config. Order matters — interceptors run in order on request, reverse order on response.HttpRequest is immutable: req.clone({ setHeaders: { Authorization: Bearer ${token} } }).next(clonedReq) to pass the (possibly modified) request down the chain. The next function returns an Observable<HttpEvent<unknown>>.catchError on the response to handle errors centrally. Return throwError(() => err) to propagate after logging, or return a recovery observable.retry({ count: 3, delay: retryStrategy }) or retryWhen. Only retry idempotent methods (GET, PUT, DELETE) — never POST by default.req.context.get(MY_TOKEN) using HttpContextToken.// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (!token) return next(req);
const authedReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(authedReq);
};
// retry.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { retry, timer } from 'rxjs';
const RETRY_METHODS = new Set(['GET', 'PUT', 'DELETE']);
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
if (!RETRY_METHODS.has(req.method)) return next(req);
return next(req).pipe(
retry({
count: 3,
delay: (error, retryCount) => {
if (error.status === 0 || error.status >= 500) {
return timer(Math.pow(2, retryCount) * 1000); // 2s, 4s, 8s
}
throw error; // Don't retry 4xx errors
},
})
);
};
// error.interceptor.ts — centralized error logging
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { NotificationService } from './notification.service';
import { AuthService } from './auth.service';
import { Router } from '@angular/router';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const notifications = inject(NotificationService);
const auth = inject(AuthService);
const router = inject(Router);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
auth.logout();
router.navigate(['/login']);
} else if (err.status === 403) {
router.navigate(['/forbidden']);
} else if (err.status >= 500) {
notifications.error('Server error. Please try again.');
}
return throwError(() => err);
})
);
};
// main.ts — register interceptors in order
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([authInterceptor, retryInterceptor, errorInterceptor])),
],
});
Functional vs class-based: Class-based interceptors implement HttpInterceptor and are registered via the HTTP_INTERCEPTORS multi-token. Functional interceptors are registered with withInterceptors(). You can mix both using withInterceptorsFromDi() alongside withInterceptors(), but prefer the functional approach for new code.
HttpContextToken for opt-out:
export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);
// In the interceptor
if (req.context.get(SKIP_AUTH)) return next(req);
// In a service — skip auth for this request
this.http.get('/public', {
context: new HttpContext().set(SKIP_AUTH, true),
});
Loading indicator pattern:
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(LoadingService);
loading.increment();
return next(req).pipe(finalize(() => loading.decrement()));
};
Token refresh (401 retry): Handle expired tokens by catching 401, refreshing the token, then retrying the original request:
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status !== 401) return throwError(() => err);
return inject(AuthService)
.refreshToken()
.pipe(
switchMap((token) => next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }))),
catchError(() => {
inject(Router).navigate(['/login']);
return throwError(() => err);
})
);
})
);
Interceptor order: Request interceptors execute top-to-bottom in the withInterceptors array. Response handling (catchError, tap on response) executes bottom-to-top. Think of it as middleware wrapping.
https://angular.dev/guide/http/interceptors