From harness-claude
Intercepts HTTP requests and responses with HttpInterceptorFn for auth headers, retry logic, loading state, and centralized error handling in Angular 15+.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:angular-http-interceptorsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Intercept HTTP requests and responses with HttpInterceptorFn for auth headers, retry logic, loading state, and centralized error handling
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
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeImplement HTTP data fetching in Angular v20+ using resource(), httpResource(), and HttpClient for API calls, data loading with signals, request/response handling, and interceptors.
Implements NestJS guards and interceptors for authentication, authorization, logging, and request/response transformation. Covers CanActivate, ExecutionContext, and JWT patterns for cross-cutting concerns.
Applies RxJS patterns in Angular: switchMap for HTTP cancellation, takeUntilDestroyed for cleanup, async pipe in templates, shareReplay for caching, catchError for resilience, and debounced typeahead search.