Skip to content

Configuration

This page is the complete configuration reference for Fetcher. Every option is documented with its source location, default value, and usage examples.

FetcherOptions

The FetcherOptions interface controls every aspect of a Fetcher instance.

PropertyTypeDefaultDescription
baseURLstring''Base URL prepended to all request paths
headersRequestHeaders{ 'Content-Type': 'application/json' }Default headers for all requests
timeoutnumberundefined (no timeout)Default timeout in milliseconds
urlTemplateStyleUrlTemplateStyleUrlTemplateStyle.UriTemplatePath parameter syntax ({param} or :param)
interceptorsInterceptorManagernew InterceptorManager(validateStatus)Custom interceptor manager (overrides defaults)
validateStatus(status: number) => booleanstatus >= 200 && status < 300Response status validation function
typescript
import { Fetcher, InterceptorManager } from '@ahoo-wang/fetcher';
import { UrlTemplateStyle } from '@ahoo-wang/fetcher';

const options: FetcherOptions = {
  baseURL: 'https://api.example.com/v2',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  timeout: 10000,
  urlTemplateStyle: UrlTemplateStyle.UriTemplate,
  validateStatus: (status) => status >= 200 && status < 300,
};

const fetcher = new Fetcher(options);

Default Options

The default configuration is defined in DEFAULT_OPTIONS:

typescript
export const DEFAULT_OPTIONS: FetcherOptions = {
  baseURL: '',
  headers: { 'Content-Type': 'application/json' },
};

baseURL

The baseURL is prepended to every request URL by the UrlBuilder. The combination logic is in combineURLs():

typescript
// Absolute request URLs bypass baseURL
await fetcher.get('https://other-api.com/data');

// Relative paths are combined
await fetcher.get('/users');
// -> https://api.example.com/users
mermaid
flowchart LR
    subgraph UrlResolution["URL Resolution"]
        style UrlResolution fill:#161b22,stroke:#30363d,color:#e6edf3
        BU["baseURL<br>https://api.example.com"]
        RP["request.url<br>/users/{id}"]
        UP["urlParams.path<br>{ id: 42 }"]
        UQ["urlParams.query<br>{ include: profile }"]
    end

    BU --> COMBINE["combineURLs()"]
    RP --> COMBINE
    COMBINE --> TEMPLATE["urlTemplateResolver.resolve()"]
    UP --> TEMPLATE
    TEMPLATE --> QUERY["URLSearchParams"]
    UQ --> QUERY
    QUERY --> FINAL["https://api.example.com/users/42?include=profile"]

timeout

The resolveTimeout function determines the effective timeout for each request. Request-level timeout takes precedence over the fetcher-level timeout:

typescript
// Fetcher-level default: 5 seconds
const fetcher = new Fetcher({ timeout: 5000 });

// Uses 5000ms (fetcher default)
await fetcher.get('/users');

// Uses 3000ms (request-level override)
await fetcher.get('/fast-endpoint', { timeout: 3000 });

// Uses 0 (no timeout for this request)
await fetcher.get('/slow-report', { timeout: 0 });

Timeout is implemented via timeoutFetch(), which creates an AbortController and races the fetch promise against a timeout promise. If the timeout fires, a FetchTimeoutError is thrown.

ScenarioBehavior
timeout: undefinedNo timeout; request runs indefinitely
timeout: 0No timeout (same as undefined)
timeout: 5000Aborts after 5000ms with FetchTimeoutError
Custom abortController providedUses that controller; timeout still applies via race
Custom signal on requestDelegates directly to fetch(); timeout is ignored

headers

Default headers are merged with per-request headers. Request headers take precedence:

typescript
const fetcher = new Fetcher({
  headers: {
    'Content-Type': 'application/json',
    'X-App-Version': '1.0.0',
  },
});

// Sends both default headers + Authorization
await fetcher.get('/protected', {
  headers: {
    Authorization: 'Bearer token123',
  },
});

// Override Content-Type for a specific request
await fetcher.post('/upload', {
  headers: { 'Content-Type': 'multipart/form-data' },
  body: formData,
});

The merge logic is in Fetcher.resolveExchange():

typescript
const mergedHeaders = { ...this.headers, ...request.headers };

URL Template Styles

Fetcher supports two path parameter syntaxes, configured via UrlTemplateStyle:

mermaid
flowchart TD
    subgraph UriTemplate["UriTemplate Style (RFC 6570)"]
        style UriTemplate fill:#161b22,stroke:#30363d,color:#e6edf3
        U1["/users/{id}/posts/{postId}"]
        U2["{ id: 123, postId: 456 }"]
        U3["/users/123/posts/456"]
        U1 --> U2 --> U3
    end

    subgraph Express["Express Style"]
        style Express fill:#161b22,stroke:#30363d,color:#e6edf3
        E1["/users/:id/posts/:postId"]
        E2["{ id: 123, postId: 456 }"]
        E3["/users/123/posts/456"]
        E1 --> E2 --> E3
    end
StyleTemplateParameters ObjectResolver Class
UrlTemplateStyle.UriTemplate (default)/users/{id}{ id: 123 }UriTemplateResolver
UrlTemplateStyle.Express/users/:id{ id: 123 }ExpressUrlTemplateResolver
typescript
import { Fetcher, UrlTemplateStyle } from '@ahoo-wang/fetcher';

// RFC 6570 style (default)
const apiFetcher = new Fetcher({ baseURL: 'https://api.example.com' });
await apiFetcher.get('/users/{id}', {
  urlParams: { path: { id: 42 } },
});

// Express style
const expressFetcher = new Fetcher({
  baseURL: 'https://api.example.com',
  urlTemplateStyle: UrlTemplateStyle.Express,
});
await expressFetcher.get('/users/:id', {
  urlParams: { path: { id: 42 } },
});

Path parameter values are automatically encoded with encodeURIComponent().

Interceptor System

The InterceptorManager manages three interceptor registries:

RegistryPhaseBuilt-in Interceptors
interceptors.requestBefore HTTP callRequestBodyInterceptor, UrlResolveInterceptor, FetchInterceptor
interceptors.responseAfter HTTP responseValidateStatusInterceptor
interceptors.errorOn errorNone (empty by default)

Built-in Request Interceptors

mermaid
sequenceDiagram
autonumber

    participant C as Client
    participant RBI as RequestBodyInterceptor
    participant URI as UrlResolveInterceptor
    participant FI as FetchInterceptor
    participant Net as Network

    C->>RBI: exchange with raw body
    RBI->>RBI: Serialize object body to JSON
    RBI->>URI: exchange
    URI->>URI: Build URL with path/query params
    URI->>FI: exchange with resolved URL
    FI->>FI: timeoutFetch(request)
    FI->>Net: HTTP request
    Net-->>FI: HTTP response
    FI-->>C: exchange with response
InterceptorOrderPurpose
RequestBodyInterceptorVery low (runs first)Converts object bodies to JSON strings, sets Content-Type
UrlResolveInterceptorVery high (runs last among request)Resolves final URL via UrlBuilder.build()
FetchInterceptorHighExecutes the actual timeoutFetch() call
ValidateStatusInterceptorNumber.MAX_SAFE_INTEGER - 10000Validates response status code

Custom Interceptor Registration

typescript
const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });

// Request interceptor
fetcher.interceptors.request.use({
  name: 'MetricsInterceptor',
  order: 200,
  async intercept(exchange) {
    exchange.attributes = exchange.attributes || new Map();
    exchange.attributes.set('startTime', Date.now());
  },
});

// Response interceptor
fetcher.interceptors.response.use({
  name: 'MetricsCollector',
  order: 200,
  async intercept(exchange) {
    const startTime = exchange.attributes.get('startTime');
    if (startTime) {
      const duration = Date.now() - startTime;
      console.log(`Request took ${duration}ms`);
    }
  },
});

// Error interceptor
fetcher.interceptors.error.use({
  name: 'RetryInterceptor',
  order: 100,
  async intercept(exchange) {
    // Implement retry logic
    if (shouldRetry(exchange.error)) {
      exchange.error = undefined; // Clear error to indicate handled
    }
  },
});

Interceptor Ordering

Interceptors execute in ascending order value. The BUILT_IN_INTERCEPTOR_ORDER_STEP is 10000, giving wide gaps for custom interceptors:

Order RangeSuggested Use
0 - 9999Before all built-in interceptors
10000 - 19999After first built-in, before second
20000 - 89999Between built-in interceptors
90000+After all built-in interceptors

Bypassing Status Validation

To skip ValidateStatusInterceptor for a specific request, set the IGNORE_VALIDATE_STATUS attribute:

typescript
import { IGNORE_VALIDATE_STATUS } from '@ahoo-wang/fetcher';

await fetcher.get('/endpoint-that-returns-404', {}, {
  attributes: { [IGNORE_VALIDATE_STATUS]: true },
});

Removing Interceptors

typescript
// Remove by name
fetcher.interceptors.request.eject('MetricsInterceptor');

// Clear all from a registry
fetcher.interceptors.error.clear();

ValidateStatus

The validateStatus function determines which HTTP status codes are treated as successful:

typescript
// Accept all status codes (never throw based on status)
const fetcher = new Fetcher({
  validateStatus: () => true,
});

// Only accept 200
const strictFetcher = new Fetcher({
  validateStatus: (status) => status === 200,
});

// Accept 2xx and 3xx
const relaxedFetcher = new Fetcher({
  validateStatus: (status) => status >= 200 && status < 400,
});

When validation fails, a HttpStatusValidationError (extending ExchangeError) is thrown with access to the full exchange object.

Result Extractors

Result extractors control what the fetcher.fetch(), get(), post(), etc. methods return. Configure them per-request or per-class:

typescript
import { ResultExtractors } from '@ahoo-wang/fetcher';

// Per-request: extract as JSON
const user = await fetcher.get<User>('/users/1', {}, {
  resultExtractor: ResultExtractors.Json,
});

// The exchange-level request method uses ResultExtractors.Exchange by default
const exchange = await fetcher.exchange({ url: '/users', method: 'GET' });
// exchange.request, exchange.response, exchange.error all accessible
ExtractorReturn TypeBest For
ExchangeResultExtractorFetchExchangeCustom processing, logging, metrics
ResponseResultExtractorResponseAccess to raw response (headers, status)
JsonResultExtractorPromise<any>JSON API responses
TextResultExtractorPromise<string>HTML, plain text
BlobResultExtractorPromise<Blob>Files, images
ArrayBufferResultExtractorPromise<ArrayBuffer>Binary protocols
BytesResultExtractorPromise<Uint8Array>Protobuf, binary data

Default result extractors by method:

MethodDefault Extractor
fetcher.fetch()ResponseResultExtractor
fetcher.get() / post() / etc.ResponseResultExtractor
fetcher.exchange()ExchangeResultExtractor
fetcher.request()ExchangeResultExtractor

NamedFetcher and FetcherRegistrar

NamedFetcher

NamedFetcher extends Fetcher and automatically registers itself with the global fetcherRegistrar:

typescript
import { NamedFetcher } from '@ahoo-wang/fetcher';

// Auto-registers as 'payments-api'
new NamedFetcher('payments-api', {
  baseURL: 'https://payments.example.com',
  timeout: 8000,
  headers: { 'X-Api-Key': 'key123' },
});

A default NamedFetcher instance named 'default' is exported from the package:

typescript
import { fetcher } from '@ahoo-wang/fetcher';
// fetcher is the 'default' named instance

FetcherRegistrar

The FetcherRegistrar is a Map<string, Fetcher> wrapper with convenience methods:

typescript
import { fetcherRegistrar } from '@ahoo-wang/fetcher';

// Register manually
fetcherRegistrar.register('custom', myFetcher);

// Retrieve
const client = fetcherRegistrar.get('custom'); // Fetcher | undefined
const required = fetcherRegistrar.requiredGet('custom'); // Fetcher (throws if missing)

// Default getter/setter
fetcherRegistrar.default = myFetcher; // registers as 'default'
const defaultClient = fetcherRegistrar.default; // requiredGet('default')

// Get all registered fetchers
const all: Map<string, Fetcher> = fetcherRegistrar.fetchers;

// Unregister
fetcherRegistrar.unregister('custom'); // boolean
mermaid
classDiagram
    class Fetcher {
        +urlBuilder: UrlBuilder
        +headers: RequestHeaders
        +timeout: number
        +interceptors: InterceptorManager
        +fetch(url, request, options) Promise
        +get(url, request, options) Promise
        +post(url, request, options) Promise
        +exchange(request, options) Promise
    }

    class NamedFetcher {
        +name: string
        +constructor(name, options)
    }

    class FetcherRegistrar {
        -registrar: Map~string, Fetcher~
        +register(name, fetcher) void
        +unregister(name) boolean
        +get(name) Fetcher | undefined
        +requiredGet(name) Fetcher
        +default: Fetcher
        +fetchers: Map~string, Fetcher~
    }

    Fetcher <|-- NamedFetcher
    NamedFetcher --> FetcherRegistrar : auto-registers
    FetcherRegistrar o-- Fetcher : manages

Environment-Specific Configuration

Use NamedFetcher to set up environment-aware clients:

typescript
import { NamedFetcher } from '@ahoo-wang/fetcher';

const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';

// Default client for your API
new NamedFetcher('default', {
  baseURL,
  timeout: 5000,
});

// Third-party API client with different config
new NamedFetcher('openai', {
  baseURL: 'https://api.openai.com/v1',
  headers: {
    Authorization: `Bearer ${import.meta.env.VITE_OPENAI_KEY}`,
  },
  timeout: 30000,
});

// Admin client with longer timeout
new NamedFetcher('admin', {
  baseURL: `${baseURL}/admin`,
  timeout: 60000,
  headers: { 'X-Admin-Token': import.meta.env.VITE_ADMIN_TOKEN },
});

Decorator Integration

The @api decorator uses FetcherRegistrar internally. When you specify fetcher: 'openai' in the decorator options, it calls fetcherRegistrar.requiredGet('openai') at decoration time:

typescript
import { NamedFetcher, fetcherRegistrar } from '@ahoo-wang/fetcher';
import { api, get } from '@ahoo-wang/fetcher-decorator';

// Must be registered before class decoration executes
new NamedFetcher('llm', { baseURL: 'https://api.openai.com/v1' });

@api('/v1/chat', { fetcher: 'llm' })
class ChatService {
  @get('/models')
  listModels(): Promise<any> {
    throw autoGeneratedError();
  }
}

Error Handling

Fetcher provides a structured error hierarchy:

mermaid
classDiagram
    class Error {
        +message: string
        +stack: string
    }

    class FetcherError {
        +name: string
        +cause: Error | unknown
    }

    class ExchangeError {
        +exchange: FetchExchange
    }

    class HttpStatusValidationError {
        +exchange: FetchExchange
    }

    class FetchTimeoutError {
        +request: FetchRequest
    }

    Error <|-- FetcherError
    FetcherError <|-- ExchangeError
    ExchangeError <|-- HttpStatusValidationError
    FetcherError <|-- FetchTimeoutError
ErrorConditionAccess To
FetchTimeoutErrorRequest exceeds timeouterror.request
HttpStatusValidationErrorStatus fails validateStatuserror.exchange (request + response)
ExchangeErrorUnhandled error in interceptor chainerror.exchange
FetcherErrorGeneral fetcher errorserror.cause
typescript
import {
  Fetcher,
  FetchTimeoutError,
  HttpStatusValidationError,
  ExchangeError,
} from '@ahoo-wang/fetcher';

try {
  await fetcher.get('/data', { timeout: 3000 });
} catch (error) {
  if (error instanceof FetchTimeoutError) {
    console.log(`Timed out after ${error.request.timeout}ms`);
  } else if (error instanceof HttpStatusValidationError) {
    console.log(`Status ${error.exchange.response?.status} failed`);
  } else if (error instanceof ExchangeError) {
    console.log(`Exchange error: ${error.message}`);
  }
}
TopicPage
Getting started with code examplesQuick Start
Project overview and architectureIntroduction
Contributing to FetcherContributing

Released under the Apache License 2.0.