Skip to content

Built-in Interceptors

Connectum provides 8 production-ready interceptors via createDefaultInterceptors(). They form a fixed chain that covers error handling, resilience, validation, and serialization.

The Default Chain

errorHandler -> timeout -> bulkhead -> circuitBreaker -> retry -> fallback -> validation -> serializer
#InterceptorPurposeDefault
1errorHandlerNormalizes errors into ConnectErrorEnabled
2timeoutLimits request execution timeEnabled (30s)
3bulkheadLimits concurrent requestsEnabled (capacity 10, queue 10)
4circuitBreakerPrevents cascading failuresEnabled (threshold 5)
5retryRetries transient failures with exponential backoffEnabled (3 retries)
6fallbackGraceful degradationDisabled
7validationValidates via @connectrpc/validateEnabled
8serializerJSON serialization for protobufEnabled

The order is deliberate: errorHandler is outermost (catches everything), serializer is innermost (closest to the handler).

Using with createServer

The recommended way to add the built-in interceptors:

typescript
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';
import { createDefaultInterceptors } from '@connectum/interceptors';

const server = createServer({
  services: [routes],
  port: 5000,
  protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
  interceptors: createDefaultInterceptors(),
  shutdown: { autoShutdown: true },
});

server.on('ready', () => {
  healthcheckManager.update(ServingStatus.SERVING);
});

await server.start();

Customizing the Default Chain

Pass options to createDefaultInterceptors() to customize individual interceptors. Set an interceptor to false to disable it entirely:

typescript
import { createDefaultInterceptors } from '@connectum/interceptors';

const interceptors = createDefaultInterceptors({
  timeout: { duration: 10_000 },   // Custom timeout (10s instead of 30s)
  retry: false,                     // Disable retry
  bulkhead: { capacity: 20, queueSize: 20 }, // Higher concurrency limits
  // All others remain at defaults
});

const server = createServer({
  services: [routes],
  port: 5000,
  protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
  interceptors,
  shutdown: { autoShutdown: true },
});

Combining with Custom Interceptors

Spread the default chain and append your own interceptors:

typescript
import { createDefaultInterceptors } from '@connectum/interceptors';

const server = createServer({
  services: [routes],
  port: 5000,
  protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
  interceptors: [
    ...createDefaultInterceptors(),
    myCustomInterceptor,  // Added after the built-in chain
  ],
  shutdown: { autoShutdown: true },
});

Auth interceptors require a specific position

If your custom interceptor is an authentication or authorization interceptor from @connectum/auth, it must be placed immediately after errorHandler -- before timeout and other resilience interceptors. See the Custom Interceptors guide for a manual chain example and ADR-024 for the rationale.

Standalone Usage

You can use createDefaultInterceptors() outside of createServer:

typescript
import { createDefaultInterceptors } from '@connectum/interceptors';

const interceptors = createDefaultInterceptors({
  timeout: { duration: 10_000 },
  retry: { maxRetries: 5 },
});

For detailed documentation on each interceptor, see the @connectum/interceptors README.

Execution Order

Interceptors execute in the order they are defined. Each interceptor wraps the next one:

Request  -> interceptor1 -> interceptor2 -> interceptor3 -> handler
Response <- interceptor1 <- interceptor2 <- interceptor3 <- handler

This means:

  • Before-logic of the first interceptor runs first
  • After-logic of the first interceptor runs last
  • The first interceptor is the outer layer (ideal for error handling)
  • The last interceptor is closest to the handler (ideal for serialization)

This is why the default chain places errorHandler first and serializer last.

Best Practices

  1. Error handler first -- place the error handler first in the chain so it catches errors from all subsequent interceptors.

  2. Do not mutate req.message -- create a new request object via spread: { ...req, message: newMessage }.

  3. Always call next() -- if the interceptor does not abort the chain, it must call next(req) and return the result.

  4. Cleanup in finally -- use try/finally for resource cleanup (timers, counters).

  5. Type safety -- use import type { Interceptor } for type-safe interceptor definitions.

  6. Use factories -- wrap interceptors in create*Interceptor(options) for configurability.

  7. skip* options for technical limitations -- options like skipStreaming and skipGrpcServices are meant for technical limitations of the interceptor, not for business routing.

  8. createMethodFilterInterceptor for routing -- use it for declarative interceptor routing by service and method.