Phtps
/Plugins
11 official plugins

The Plugin Ecosystem

Every capability is opt-in. Add exactly what you need — auth, retry, caching, encryption — and nothing else. All plugins are tree-shakeable.

Plugin System

Plugins extend Phtps without modifying the core. Every advanced feature — auth, retry, caching, encryption — is a plugin you opt into. The core client stays tiny and fast.

ts
import { createHttpClient } from 'phtps';
import { AuthPlugin, RetryPlugin, CachePlugin } from 'phtps/plugins';

const client = createHttpClient({ baseURL: 'https://api.example.com' });

client.use(AuthPlugin());
client.use(RetryPlugin());
client.use(CachePlugin());
Modify requests
Modify responses
Handle errors
Add headers
Retry requests
Cache responses
Encrypt payloads
Queue requests
Paginate data

Authentication

AuthPlugin

Authentication

Automatic token injection, 401 interception, and single-lock refresh.

ts
import { AuthPlugin } from 'phtps/plugins';

client.use(
  AuthPlugin({
    getAccessToken: () => localStorage.getItem('token'),

    onTokenRefresh: async () => {
      const { token } = await refreshToken();
      localStorage.setItem('token', token);
      return token;
    },

    onRefreshFailure: (error) => {
      localStorage.removeItem('token');
      window.location.href = '/login';
    },
  })
);
Automatic token injectionSingle refresh lockConcurrent request safetyQueue pending requestsRefresh only once

401 Refresh Flow

Request fired
client.get('/api/me')
401 Detected
server returned Unauthorized
Acquire refresh lock
single lock — one refresh only
Refresh token
onTokenRefresh() called once
Replay queued requests
all waiting requests resume
Response
caller receives data

TokenRotationPlugin

Authentication

Proactively rotate tokens before they expire — no 401s needed.

Unlike AuthPlugin (which reacts to 401s), TokenRotationPlugin is proactive. It reads the token’s expiration, starts a background timer, and refreshes the token before it expires. All concurrent requests wait behind a single refresh lock.

ts
import { AuthPlugin } from 'phtps/plugins';

client.use(
  AuthPlugin({
    tokenRotation: {
      enabled: true,
      getToken: () => localStorage.getItem('token') ?? undefined,
      getExpiration: (token) => {
        const { exp } = JSON.parse(atob(token.split('.')[1]));
        return exp * 1000; // to ms epoch
      },
      onRefresh: async () => {
        const { token } = await refreshToken();
        localStorage.setItem('token', token);
        return token;
      },
      refreshWindow: 60000,         // refresh 60s before expiry
      autoRefreshBackground: true,  // start a background timer
      headerName: 'Authorization',
      headerPrefix: 'Bearer ',
    },
  })
);
Proactive refresh (no 401s)Background timerSingle refresh lockConfigurable refresh windowSupports custom header names

Reliability

RetryPlugin

Reliability

Exponential backoff with jitter. Never retries auth errors or cancellations.

ts
import { RetryPlugin } from 'phtps/plugins';

client.use(
  RetryPlugin({ retries: 3 })
);

// Per-request overrides
await client.get('/api/data', {
  retries: 5,
  retryDelay: 1000,                              // base delay in ms
  retryDelay: (attempt) => attempt * 2000,        // or a custom fn
  retryCondition: (err) => err.status === 429,   // custom predicate
});
Exponential backoff±20% jitterNetwork retry5xx retryCustom delay fnCustom retry conditionsNever retries 4xx or cancellations

TimeoutPlugin

Reliability

Per-request timeout with automatic AbortController cleanup.

Automatically aborts requests that exceed a time limit. Properly tears down the AbortController — no dangling promises or memory leaks.

ts
// Set globally on the client
const client = createHttpClient({
  timeout: 10_000, // 10 seconds (default)
});

// Override per request
const data = await client.get('/slow-endpoint', {
  timeout: 30_000,
});

// Disable timeout for one request
await client.get('/long-poll', {
  timeout: 0,
});
Global defaultPer-request overrideAbortController cleanupNo dangling promisesWorks with streaming

Performance

CachePlugin

Performance

TTL-based GET caching with swappable adapters. Memory, LocalStorage, or custom.

ts
import { CachePlugin } from 'phtps/plugins';

// In-memory (default)
client.use(CachePlugin());

// Persistent across page loads
import { LocalStorageCacheAdapter } from 'phtps';
client.use(CachePlugin({
  adapter: new LocalStorageCacheAdapter(),
}));

// Per-request
await client.get('/api/users', {
  useCache: true,
  cacheTTL: 300_000, // 5 minutes
});

// Invalidate manually
client.clearCache();
Memory cacheLocalStorage adapterSessionStorage adapterCustom adapter interfaceTTL per requestManual invalidation

QueuePlugin

Performance

Concurrency control to prevent request storms and respect rate limits.

ts
import { QueuePlugin } from 'phtps/plugins';

client.use(QueuePlugin({ concurrency: 3 }));

// Control at runtime
const queue = client.queueManager;

queue.pause();             // stop new tasks
queue.resume();            // resume
queue.setConcurrency(5);   // change limit live
queue.clear();             // reject all queued tasks

// Bypass queue for one request
await client.get('/urgent', { queue: false });
Configurable concurrencyFIFO orderingPause / resumeLive concurrency updatesPer-request bypass

DedupPlugin

Performance

Collapse identical in-flight GET requests into a single network call.

If three components call client.get('/user/me') at the same time, only one request leaves the browser. All three callers receive the same resolved value.

ts
// Deduplication is ON by default for GET requests.
// No plugin needed — but you can control it:

// Disable for one request (force a fresh call)
await client.get('/api/user', { deduplicate: false });

// Explicitly install the plugin to dedup at middleware level
import { DedupePlugin } from 'phtps/plugins';
client.use(DedupePlugin());
Built-in for GET by defaultZero configRace-condition safePer-request opt-out

Security

EncryptionPlugin

Security

End-to-end AES-GCM payload encryption using the native Web Crypto API.

ts
import { EncryptionPlugin } from 'phtps/plugins';

client.use(
  EncryptionPlugin({
    secret: getUserKey(), // Inject at runtime — never hardcode
  })
);

// Per-request
await client.post('/api/sensitive', { ssn: '123-45-6789' }, {
  encryptionKey: runtimeKey,
  encryptPayload: true,
  decryptResponse: true,
});
AES-GCMPBKDF2 key derivationRandom IV per requestRandom saltWeb Crypto APIZero dependenciesKey stripped from error logs
Always inject encryptionKey at runtime — from sessionStorage, a user passphrase, or a server-issued key. Never bundle it or put it in .env.

CsrfPlugin

Security

Auto-read CSRF tokens from cookies or meta tags and inject them on mutating requests.

Built into the core — no explicit plugin install needed. Works out of the box with Rails, Django, Laravel, and any custom CSRF implementation.

ts
// Enable globally
const client = createHttpClient({
  csrf: {
    enabled: true,
    cookieName: 'XSRF-TOKEN',    // read from this cookie
    headerName: 'X-XSRF-TOKEN', // send as this header
  },
});

// Or configure per-request
await client.post('/api/action', data, {
  csrf: {
    enabled: true,
    cookieName: 'csrf-token',
    headerName: 'X-CSRF-Token',
    methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
    token: () => getCsrfToken(),
    onTokenMissing: () => fetchNewCsrfToken(),
  },
});
Cookie extractionMeta tag extractionDouble-submit cookie patternCustom header nameFramework presets

PaymentPlugin

SecurityFeatured

Payment-grade security — HMAC signing, idempotency keys, PII masking, and replay protection.

This plugin is unusual — it is the only one designed specifically for financial APIs. It combines everything a payment integration needs into one install.

ts
import { PaymentPlugin } from 'phtps/plugins';

const paymentClient = createHttpClient({
  baseURL: 'https://payments.example.com',
});

paymentClient.use(PaymentPlugin({
  environment: 'production',
  secretKey: getRuntimeKey(),   // HMAC signing secret
  signRequests: true,           // HMAC-SHA256 → X-Signature header
  idempotency: true,            // Auto Idempotency-Key per request
  timestamp: true,              // X-Timestamp replay prevention
  maskSensitiveData: true,      // Strip card / cvv / account from errors
  rateLimit: {
    maxRequests: 10,
    windowMs: 60_000,           // 10 req/min per process
  },
}));

// Per-request: bring your own idempotency key
await paymentClient.post('/charge', payload, {
  payment: {
    idempotencyKey: 'order-abc-123',
    skipRateLimit: true,
  },
});
HMAC Signatures

Signs every request with HMAC-SHA256. Server can verify the payload hasn't been tampered with.

Idempotency Keys

Auto-generates a UUID per request. Prevent duplicate charges if the network hiccups.

Sensitive Field Masking

Strips card number, CVV, account numbers from any error thrown. PII never leaks into logs.

Timestamp Validation

Injects X-Timestamp header. Servers can reject requests older than N seconds.

Data

PaginationPlugin

Data

Auto-paginate any API — page, offset, or cursor — and merge all results automatically.

Page?page=1&pageSize=20
Offset?offset=20&limit=20
Cursor?cursor=eyJpZCI6MTIzfQ
ts
import { PaginationPlugin } from 'phtps/plugins';

client.use([PaginationPlugin(), CachePlugin()]);

// Page-based — merges all pages into data[]
const { data } = await client.get('/api/posts', {
  paginate: { strategy: 'page', limit: 100, pageSize: 20 },
});

// Cursor-based
const { data } = await client.get('/api/feed', {
  paginate: { strategy: 'cursor', cursorField: 'nextCursor', limit: 200 },
});

// Offset-based
const { data } = await client.get('/api/logs', {
  paginate: { strategy: 'offset', pageSize: 50, limit: 500 },
});

// Prefetch mode — silently loads next pages into cache
const { data } = await client.get('/api/posts', {
  useCache: true,
  paginate: {
    mode: 'prefetch',
    strategy: 'page',
    limit: 3,
    prefetchStrategy: 'idle', // 'immediate' | 'adaptive' | 'idle'
  },
});
Page, offset, cursor strategiesAuto-merge all itemsPrefetch next pagesSafety limitsCustom getItems / hasNextPage

Writing Custom Plugins

Any object with a name string and an install(client) function is a valid Phtps plugin. You get full access to interceptors, middleware, config, and the cache/queue managers.

ts
function LoggerPlugin() {
  return {
    name: "logger",

    install(client) {
      client.interceptors.request.use(config => {
        console.log(`→ ${config.method} ${config.url}`);
        return config;
      });

      client.interceptors.response.use(
        response => {
          console.log(`← ${response.status} ${response.config.url}`);
          return response;
        },
        error => {
          console.error(`✗ ${error.response?.status} ${error.config?.url}`);
          return Promise.reject(error);
        }
      );
    }
  };
}

// Usage
client.use(LoggerPlugin());

What you can access inside install(client)

Property / MethodDescription
interceptors.requestAdd request interceptors
interceptors.responseAdd response interceptors
useMiddleware(fn)Add to the middleware pipeline
setConfig(config)Merge config into the client
request(config)Make a request from inside the plugin
cacheManagerAccess cache (if CachePlugin installed)
queueManagerAccess queue (if QueuePlugin installed)
tokenRotationManagerAccess proactive token rotation

Plugin Order

Important

The order you call use() is the order plugins wrap the request. Think of it as an onion — the first plugin installed is the outermost layer.

ts
client.use(AuthPlugin());        // 1. Outermost — handles tokens
client.use(CsrfPlugin());        // 2. Injects CSRF header
client.use(EncryptionPlugin());  // 3. Encrypts body close to wire
client.use(CachePlugin());       // 4. Caches successful responses
client.use(RetryPlugin());       // 5. Innermost — retries failed requests

Request Pipeline

1
Auth
2
CSRF
3
Encryption
4
Cache
5
Retry
6
Network

Response Pipeline

1
Network
2
Retry
3
Cache
4
Decrypt
5
Response

Why this order matters

Auth firstTokens are injected before any other plugin modifies or encrypts the request.
CSRF after AuthThe CSRF token is added after auth headers — some CSRF implementations read the auth state.
Encryption close to wireEncrypts the final payload. Running it earlier would encrypt headers you haven't set yet.
Cache after RetryOnly successful responses (post-retry) are worth caching.
Retry innermostRetries only the network call, not the full plugin pipeline. Efficient and correct.

Best Practices

Use Auth before Retry

Auth resolves tokens; Retry wraps the network call.

Use Encryption after Auth

Encrypt the final payload with all headers set.

Cache only GET requests

POST/PUT/DELETE are mutating — caching them causes stale data.

Avoid mutating config objects

Return a new config or call config.headers.set() — do not mutate.

Keep plugins stateless

Store state on the client (cacheManager, queueManager) not in closures.

Cleanup listeners on destroy

Remove interceptors and timers when client.destroy() is called.

Handle refresh failures

Always provide onRefreshFailure to redirect to login on hard auth failure.

Write plugin tests

Plugins are isolated — unit test them with a mock client.

Start with the official plugins

All plugins tree-shake. Import only what you use.