
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
@voyagerpoland/proxy
Advanced tools
HTTP service proxy with Result Pattern integration for Angular. Wraps Angular HttpClient calls into ResultAsync<T> with built-in retry and circuit breaker support.
npm install @voyagerpoland/proxy
npm install @voyagerpoland/results neverthrow rxjs @angular/common
fromHttp<T>(observable, errorMapper?)Converts an Angular HTTP Observable<T> into a ResultAsync<T>.
import { fromHttp } from '@voyagerpoland/proxy';
const result = await fromHttp(httpClient.get<User>('/api/users/1'));
// Result<User> — Ok({ id: 1, name: 'Alice' }) or Err(AppError)
fromHttpError(error)Converts an HttpErrorResponse (or compatible object) to an AppError with HTTP context.
import { fromHttpError } from '@voyagerpoland/proxy';
const appError = fromHttpError(httpErrorResponse);
// appError.type === ErrorType.NotFound (for 404)
// appError.context.get('url') === '/api/users/42'
// appError.context.get('statusText') === 'Not Found'
ServiceProxy (abstract class)Abstract base class for Angular HTTP service proxies with built-in resilience. Retry and circuit breaker are enabled by default — no configuration needed.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ServiceProxy } from '@voyagerpoland/proxy';
import type { ResultAsync } from '@voyagerpoland/results';
import { API_BASE_URL } from '../api-config';
// DTO: automatically generated from OpenAPI
import type { Order, CreateOrderRequest, OrderListResponse } from '../generated/models';
@Injectable({ providedIn: 'root' })
class OrderService extends ServiceProxy {
constructor() {
super(inject(HttpClient), inject(API_BASE_URL));
}
getOrder(id: number): ResultAsync<Order> {
return this.get<Order>(`/orders/${id}`);
}
listOrders(status?: string, limit?: number): ResultAsync<OrderListResponse> {
return this.get<OrderListResponse>('/orders', {
status,
limit: limit?.toString(),
});
}
createOrder(request: CreateOrderRequest): ResultAsync<Order> {
return this.post<Order>('/orders', request);
}
deleteOrder(id: number): ResultAsync {
return this.del(`/orders/${id}`);
}
}
3 lines per endpoint. DTO automatic from OpenAPI. Retry + circuit breaker for free.
@Component({
/* ... */
})
class UserComponent {
private readonly orders = inject(OrderService);
loadOrder(id: number): void {
this.orders.getOrder(id).match(
(order) => this.order.set(order),
(error) => this.error.set(error.message),
);
}
}
ServiceProxyOptionsAll fields are optional — when omitted, sensible defaults are used:
interface ServiceProxyOptions {
maxRetryAttempts?: number; // default: 3
retryBaseDelayMs?: number; // default: 1000
circuitBreakerThreshold?: number; // default: 5
circuitBreakerTimeoutMs?: number; // default: 30000
authHandler?: AuthHandler; // default: undefined (no auth handling)
}
Custom configuration example:
super(inject(HttpClient), inject(API_BASE_URL), {
maxRetryAttempts: 5,
retryBaseDelayMs: 500,
circuitBreakerThreshold: 10,
circuitBreakerTimeoutMs: 60_000,
});
AuthHandlerOptional strategy for reacting to HTTP 401 (Unauthorized) errors. When configured, ServiceProxy calls onUnauthorized() before returning an error, giving the handler a chance to refresh credentials and retry the request.
interface AuthHandler {
onUnauthorized(): Promise<{ retry: boolean }>;
}
Flow:
Request → 401
→ authHandler defined?
→ NO: Err(Unauthorized) immediately (default behavior)
→ YES: await authHandler.onUnauthorized()
→ { retry: true } → retry request once (with refreshed credentials)
→ success → Ok(T)
→ 401 again → Err(Unauthorized) — prevents infinite loop
→ { retry: false } → Err(Unauthorized)
Key design decisions:
class BffAuthHandler implements AuthHandler {
constructor(private window: Window) {}
async onUnauthorized(): Promise<{ retry: boolean }> {
const returnUrl = this.window.location.pathname;
this.window.location.href = `/bff/login?returnUrl=${encodeURIComponent(returnUrl)}`;
// Redirect leaves the SPA — never resolves
return { retry: false };
}
}
@Injectable({ providedIn: 'root' })
class OrderService extends ServiceProxy {
constructor() {
super(inject(HttpClient), inject(API_BASE_URL), {
authHandler: new BffAuthHandler(window),
});
}
}
class TokenAuthHandler implements AuthHandler {
private refreshInProgress: Promise<{ retry: boolean }> | null = null;
constructor(
private authService: AuthService,
private router: Router,
) {}
async onUnauthorized(): Promise<{ retry: boolean }> {
// Single-refresh guard — concurrent 401s share one refresh
if (this.refreshInProgress) {
return this.refreshInProgress;
}
this.refreshInProgress = this.doRefresh();
try {
return await this.refreshInProgress;
} finally {
this.refreshInProgress = null;
}
}
private async doRefresh(): Promise<{ retry: boolean }> {
try {
await this.authService.refreshToken();
return { retry: true };
} catch {
this.router.navigate(['/login'], {
queryParams: { returnUrl: this.router.url },
});
return { retry: false };
}
}
}
After a successful refresh, ServiceProxy retries the original HttpClient call. Angular interceptors run again on the new request, automatically attaching the refreshed Bearer token.
ProxyDiagnosticsOptional callback-based telemetry hooks. When configured, ServiceProxy emits events for request lifecycle, retry attempts, and circuit breaker state changes. Vendor-neutral — wire to Application Insights, OpenTelemetry, or any provider.
interface ProxyDiagnostics {
onRequestStart?: (event: RequestStartEvent) => void;
onRequestComplete?: (event: RequestCompleteEvent) => void;
onRequestFail?: (event: RequestFailEvent) => void;
onRetryAttempt?: (event: RetryAttemptEvent) => void;
onCircuitBreakerStateChanged?: (event: CircuitBreakerStateChangedEvent) => void;
}
Configuration:
super(inject(HttpClient), inject(API_BASE_URL), {
diagnostics: {
onRequestStart: (e) => console.log(`${e.method} ${e.url}`),
onRequestComplete: (e) =>
console.log(`${e.method} ${e.url} ${e.durationMs}ms (${e.retryAttempts} retries)`),
onRequestFail: (e) => console.error(`${e.method} ${e.url} failed: ${e.error.type}`),
onRetryAttempt: (e) => console.warn(`Retry #${e.attempt} for ${e.url}, delay ${e.delayMs}ms`),
onCircuitBreakerStateChanged: (e) => console.warn(`Circuit: ${e.oldState} → ${e.newState}`),
},
});
| Event | Fields | When |
|---|---|---|
RequestStartEvent | method, url, startedAt | Before first HTTP attempt |
RequestCompleteEvent | method, url, durationMs, retryAttempts | After successful response (including retries) |
RequestFailEvent | method, url, durationMs, retryAttempts, error | After all retries exhausted |
RetryAttemptEvent | method, url, attempt, delayMs, error | On each retry attempt |
CircuitBreakerStateChangedEvent | oldState, newState, failureCount, lastError | When circuit breaker transitions |
import { ApplicationInsights } from '@microsoft/applicationinsights-web';
import type { ProxyDiagnostics } from '@voyagerpoland/proxy';
export function createAppInsightsDiagnostics(appInsights: ApplicationInsights): ProxyDiagnostics {
return {
onRequestComplete: (event) => {
appInsights.trackDependencyData({
id: crypto.randomUUID(),
name: `${event.method} ${event.url}`,
duration: event.durationMs,
success: true,
responseCode: 200,
properties: { retryAttempts: String(event.retryAttempts) },
});
},
onRequestFail: (event) => {
appInsights.trackDependencyData({
id: crypto.randomUUID(),
name: `${event.method} ${event.url}`,
duration: event.durationMs,
success: false,
responseCode: Number(event.error.code.replace('HTTP_', '')) || 0,
properties: {
retryAttempts: String(event.retryAttempts),
errorType: event.error.type,
},
});
},
onRetryAttempt: (event) => {
appInsights.trackEvent({
name: 'ServiceProxy.RetryAttempt',
properties: {
url: event.url,
attempt: String(event.attempt),
errorType: event.error.type,
},
});
},
onCircuitBreakerStateChanged: (event) => {
appInsights.trackEvent({
name: 'ServiceProxy.CircuitBreakerStateChanged',
properties: {
oldState: event.oldState,
newState: event.newState,
failureCount: String(event.failureCount),
},
});
},
};
}
composeDiagnostics(...providers)Combines multiple ProxyDiagnostics providers into one. Each event is forwarded to all providers that define the corresponding callback.
import { composeDiagnostics } from '@voyagerpoland/proxy';
import type { ProxyDiagnostics } from '@voyagerpoland/proxy';
// Provider 1: console logging (dev)
function consoleDiagnostics(): ProxyDiagnostics {
return {
onRequestStart: (e) => console.log(`→ ${e.method} ${e.url}`),
onRequestComplete: (e) => console.log(`✓ ${e.method} ${e.url} ${e.durationMs}ms`),
onRequestFail: (e) => console.error(`✗ ${e.method} ${e.url} failed: ${e.error.type}`),
onRetryAttempt: (e) => console.warn(`↻ Retry #${e.attempt} for ${e.url}`),
};
}
// Provider 2: Application Insights (prod) — see adapter example above
// Compose both
const diagnostics = composeDiagnostics(
consoleDiagnostics(),
createAppInsightsDiagnostics(appInsights),
);
super(inject(HttpClient), inject(API_BASE_URL), { diagnostics });
Providers that don't define a callback for a given event are simply skipped. You can compose any number of providers.
@voyagerpoland/proxy provides the ServiceProxy base class. The consumer project is responsible for generating TypeScript DTOs from the backend's OpenAPI spec. Together they give 3 lines per endpoint with full type safety.
┌─────────────────────────────────────────────────┐
│ Backend .NET │
│ ASP.NET Core → swagger.json (automatically) │
└──────────────────────┬──────────────────────────-┘
│
▼
┌──────────────────────────────────────────────────┐
│ ng-openapi-gen (build-time) │
│ swagger.json → TypeScript DTO (models only) │
│ │
│ Generated: │
│ - models/order.ts (interface Order) │
│ - models/create-order-request.ts │
│ - models/order-list-response.ts │
└──────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ Service facade (hand-written, ~3 lines/endpoint)│
│ │
│ class OrderService extends ServiceProxy { │
│ getOrder(id) = this.get<Order>(`/orders/${id}`)│
│ } │
└──────────────────────────────────────────────────┘
npm install -D ng-openapi-gen
Download swagger.json from the backend (or point to a URL):
curl -o swagger.json https://api.example.com/swagger/v1/swagger.json
Create ng-openapi-gen.json in the project root:
{
"$schema": "node_modules/ng-openapi-gen/ng-openapi-gen-schema.json",
"input": "swagger.json",
"output": "src/app/api/generated",
"ignoreUnusedModels": false,
"services": false
}
"services": false— generate only models, not services. Service facades are hand-written withServiceProxy.
{
"scripts": {
"api:generate": "ng-openapi-gen --config ng-openapi-gen.json",
"api:update": "curl -o swagger.json https://api.example.com/swagger/v1/swagger.json && npm run api:generate",
"prebuild": "npm run api:generate"
}
}
npm run api:generate
This produces files under src/app/api/generated/models/:
src/app/api/generated/
models/
order.ts ← interface Order { id: number; ... }
create-order-request.ts ← interface CreateOrderRequest { ... }
order-list-response.ts ← interface OrderListResponse { ... }
// src/app/api/api-config.ts
import { InjectionToken } from '@angular/core';
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
// app.config.ts
import { API_BASE_URL } from './api/api-config';
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient(), { provide: API_BASE_URL, useValue: 'https://api.example.com' }],
};
// src/app/api/order.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import type { ResultAsync } from '@voyagerpoland/results';
import { ServiceProxy } from '@voyagerpoland/proxy';
import { API_BASE_URL } from './api-config';
// Types generated from OpenAPI (Step 5)
import type { Order, CreateOrderRequest, OrderListResponse } from './generated/models';
@Injectable({ providedIn: 'root' })
export class OrderService extends ServiceProxy {
constructor() {
super(inject(HttpClient), inject(API_BASE_URL));
}
getOrder(id: number): ResultAsync<Order> {
return this.get<Order>(`/orders/${id}`);
}
listOrders(status?: string, limit?: number): ResultAsync<OrderListResponse> {
return this.get<OrderListResponse>('/orders', {
status,
limit: limit?.toString(),
});
}
createOrder(request: CreateOrderRequest): ResultAsync<Order> {
return this.post<Order>('/orders', request);
}
deleteOrder(id: number): ResultAsync {
return this.del(`/orders/${id}`);
}
}
@Component({
/* ... */
})
export class OrderListComponent {
private readonly orderService = inject(OrderService);
orders = signal<Order[]>([]);
error = signal<string | null>(null);
loadOrders(): void {
this.orderService.listOrders('active', 50).match(
(response) => this.orders.set(response.items),
(error) => this.error.set(error.message),
);
}
}
src/app/api/
generated/ ← ng-openapi-gen output (git-ignored or committed)
models/
order.ts
create-order-request.ts
order-list-response.ts
...
api-config.ts ← API_BASE_URL injection token
order.service.ts ← extends ServiceProxy + generated DTO
payment.service.ts
user.service.ts
| Step | Time | Who |
|---|---|---|
| Add endpoint in C# | 5 min | Backend developer |
| swagger.json regenerates automatically | 0 min | CI/CD |
npm run api:generate regenerates DTO | 0 min | npm script |
| Add 1 method in service facade (3 lines) | 1 min | Frontend developer |
| Total | ~6 min |
If ng-openapi-gen doesn't fit your needs:
| Generator | Angular HttpClient | DTO | Requires | Notes |
|---|---|---|---|---|
| ng-openapi-gen | Yes | Yes | Node.js | Recommended, lightweight |
| openapi-generator | Yes | Yes | JVM | More mature, heavier |
| NSwag | Yes | Yes | .NET SDK | Best .NET integration |
All generators work the same way: generate only models ("services": false), write service facades with ServiceProxy.
| Status | ErrorType | Classification |
|---|---|---|
| 0 | Unavailable | Transient |
| 400 | Validation | Business |
| 401 | Unauthorized | Business* |
| 403 | Permission | Business |
| 404 | NotFound | Business |
| 409 | Conflict | Business |
| 422 | Business | Business |
| 429 | TooManyRequests | Transient |
| 503 | Unavailable | Transient |
| 504 | Timeout | Transient |
| Other | Unexpected | Infrastructure |
* When authHandler is configured, 401 triggers onUnauthorized() before returning Err. See AuthHandler.
MIT
FAQs
HTTP service proxy with Result Pattern integration for Angular
We found that @voyagerpoland/proxy demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.