Skip to content
Back to skills

Angular Http Resilience

ASecurity

Typed HttpClient wrappers, interceptor-based retry with exponential backoff and per-request timeout, correlation-ID header propagation to .NET backends, error normalisation into typed error models, and loading-state management. Targets Angular 17+ functional HttpClient configuration.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 6, 2026
developmenttypescriptgoangularapifrontendbackendsecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill angular-http-resilience --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Angular Http Resilience?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Angular Http Resilience
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-angular-http-resilience/badge)](https://www.skillsdirectory.com/skills/agenticpawan-angular-http-resilience)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: angular-http-resilience
description: Typed HttpClient wrappers, interceptor-based retry with exponential backoff and per-request timeout, correlation-ID header propagation to .NET backends, error normalisation into typed error models, and loading-state management. Targets Angular 17+ functional HttpClient configuration.
when_to_use: HTTP, HttpClient, interceptor, retry, timeout, error handling, correlation ID, request header, loading state, API calls, http resilience, typed response, catchError, exponential backoff, HttpContext, withInterceptors
applies_to: angular>=17
---

<!-- Version index:
  provideHttpClient + withInterceptors  Angular 15+
  HttpContextToken                      Angular 12+
  functional interceptors               Angular 15+
  withXsrfConfiguration()               Angular 15+
  httpResource()                        experimental Angular 19.2+
-->

## Architecture overview

```
Component / Service
    │  typed request method
    ▼
ApiService (typed wrapper)
    │  Observable<T>
    ▼
HttpClient  ──► Interceptor chain ──► .NET API
              ├─ CorrelationIdInterceptor   (adds X-Correlation-Id header)
              ├─ RetryInterceptor           (exponential backoff on 5xx)
              └─ TimeoutInterceptor         (per-request timeout via HttpContext)
```

---

## Typed service wrapper

### BAD — scattered HttpClient calls with `any`

```typescript
@Component({ ... })
export class ProductsComponent {
  products: any[] = [];

  constructor(private http: HttpClient) {
    // No typing, no error handling, no cleanup
    this.http.get('/api/products').subscribe((data: any) => {
      this.products = data;
    });
  }
}
```

### GOOD — typed service with `Observable<T>`

```typescript
// product.service.ts
export interface Product { id: number; name: string; price: number; }
export interface ApiError  { code: string; message: string; traceId: string; }

@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);
  private base = '/api/products';

  getAll(): Observable<Product[]> {
    return this.http.get<Product[]>(this.base);
  }

  getById(id: number): Observable<Product> {
    return this.http.get<Product>(`${this.base}/${id}`);
  }

  create(payload: Omit<Product, 'id'>): Observable<Product> {
    return this.http.post<Product>(this.base, payload);
  }
}
```

**Note:** `http.get<T>()` is a type assertion, not runtime validation. Validate with
`zod` or a custom validator when schema correctness is critical.

---

## Correlation-ID interceptor

Propagates a per-request ID from Angular to the .NET backend so distributed traces
link frontend logs to backend logs.

```typescript
// correlation-id.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';

export const correlationIdInterceptor: HttpInterceptorFn = (req, next) => {
  const correlationId = crypto.randomUUID();
  const cloned = req.clone({
    headers: req.headers.set('X-Correlation-Id', correlationId)
  });
  return next(cloned);
};
```

**.NET backend — read and echo the header:**
```csharp
// Middleware or filter
var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault()
    ?? Guid.NewGuid().ToString();
ctx.Response.Headers.Append("X-Correlation-Id", correlationId);
// Enrich logger scope:
using (_logger.BeginScope(new { CorrelationId = correlationId })) { ... }
```

---

## Retry interceptor with exponential backoff

```typescript
// retry.interceptor.ts
import { HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { timer } from 'rxjs';
import { retry, switchMap } from 'rxjs/operators';

const RETRYABLE_STATUS = new Set([502, 503, 504]);
const MAX_RETRIES = 3;

export const retryInterceptor: HttpInterceptorFn = (req, next) => {
  // Only retry idempotent methods
  if (!['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next(req);
  }

  return next(req).pipe(
    retry({
      count: MAX_RETRIES,
      delay: (error, attempt) => {
        if (!RETRYABLE_STATUS.has(error.status)) {
          throw error;  // non-retryable: propagate immediately
        }
        const backoff = Math.pow(2, attempt) * 200;  // 400ms, 800ms, 1600ms
        return timer(backoff);
      }
    })
  );
};
```

---

## Per-request timeout via HttpContextToken

```typescript
// timeout.interceptor.ts
import {
  HttpContextToken, HttpInterceptorFn, HttpErrorResponse
} from '@angular/common/http';
import { inject } from '@angular/core';
import { timeout, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

/** Pass TIMEOUT_MS(5000) in HttpContext to override the default 10 s timeout. */
export const TIMEOUT_MS = new HttpContextToken<number>(() => 10_000);

export const timeoutInterceptor: HttpInterceptorFn = (req, next) => {
  const ms = req.context.get(TIMEOUT_MS);
  return next(req).pipe(
    timeout(ms),
    catchError(err => {
      if (err.name === 'TimeoutError') {
        return throwError(() =>
          new HttpErrorResponse({ status: 408, statusText: 'Request Timeout' })
        );
      }
      return throwError(() => err);
    })
  );
};
```

**Caller usage — override timeout per request:**
```typescript
import { HttpContext } from '@angular/common/http';
import { TIMEOUT_MS } from './timeout.interceptor';

this.http.get<Report>('/api/report/generate', {
  context: new HttpContext().set(TIMEOUT_MS, 30_000)  // 30 s for slow report
});
```

---

## Register interceptors

```typescript
// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        correlationIdInterceptor,  // first: add ID to every outgoing request
        retryInterceptor,          // second: retry 5xx on idempotent methods
        timeoutInterceptor         // third: cancel requests that exceed timeout
      ]),
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',
        headerName: 'X-XSRF-TOKEN'
      })
    )
  ]
};
```

---

## Error normalisation

Map `HttpErrorResponse` to a domain error model so components never inspect raw HTTP status codes:

```typescript
// api-error.ts
export interface ApiError {
  status:   number;
  message:  string;
  traceId?: string;
}

export function toApiError(err: HttpErrorResponse): ApiError {
  return {
    status:  err.status,
    message: err.error?.message ?? err.statusText,
    traceId: err.error?.traceId ?? err.headers.get('X-Correlation-Id') ?? undefined
  };
}
```

```typescript
// product.service.ts — surface normalised error
getAll(): Observable<Product[]> {
  return this.http.get<Product[]>(this.base).pipe(
    catchError((err: HttpErrorResponse) => throwError(() => toApiError(err)))
  );
}
```

---

## Loading state with signals

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (loading()) { <app-spinner /> }
    @else if (error()) { <p role="alert">{{ error()!.message }}</p> }
    @else {
      @for (p of products(); track p.id) { <app-product-card [product]="p" /> }
    }
  `
})
export class ProductListComponent {
  private svc = inject(ProductService);

  products = signal<Product[]>([]);
  loading  = signal(true);
  error    = signal<ApiError | null>(null);

  constructor() {
    this.svc.getAll().pipe(takeUntilDestroyed()).subscribe({
      next:     p   => { this.products.set(p); this.loading.set(false); },
      error:    err => { this.error.set(err);  this.loading.set(false); }
    });
  }
}
```

---

## Checklist

- [ ] All HTTP calls go through a typed service method — no raw `HttpClient` in components
- [ ] `correlationIdInterceptor` registered — every request carries `X-Correlation-Id`
- [ ] `retryInterceptor` retries only idempotent methods (GET/HEAD/OPTIONS) on 5xx
- [ ] `timeoutInterceptor` guards all requests; override with `TIMEOUT_MS` for slow endpoints
- [ ] `HttpErrorResponse` mapped to domain `ApiError` before reaching the component
- [ ] Loading, error, and data states are distinct signals — template handles all three
- [ ] Non-idempotent (POST/PUT/DELETE) retries require idempotency keys from the .NET API
- [ ] `withXsrfConfiguration()` names match `.NET AntiforgeryOptions` (see angular-security)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…