Skip to content

api‐zimic‐interceptor‐http

github-actions[bot] edited this page Mar 21, 2025 · 11 revisions

@zimic/interceptor/http - API reference

Contents


zimic/interceptor/http exports resources to create HTTP interceptors and mock HTTP responses.

HttpInterceptor

HTTP interceptors provide the main API to handle HTTP requests and return mock responses. The methods, paths, status codes, parameters, and responses are statically-typed based on the service schema.

Each interceptor represents a service and can be used to mock its paths and methods.

createHttpInterceptor(options)

Creates an HTTP interceptor, the main interface to intercept HTTP requests and return responses. Learn more about declaring interceptor schemas.

Tip

If you are using TypeScript and have an OpenAPI v3 schema, you can use zimic-http typegen to automatically generate types for your interceptor schema!

Creating a local HTTP interceptor

A local interceptor is configured with type: 'local'. The baseURL represents the URL should be matched by this interceptor. Any request starting with the baseURL will be intercepted if a matching handler exists.

import { createHttpInterceptor } from '@zimic/interceptor/http';

interface User {
  username: string;
}

const interceptor = createHttpInterceptor<{
  '/users/:id': {
    GET: {
      response: {
        200: { body: User };
      };
    };
  };
}>({
  type: 'local',
  baseURL: 'http://localhost:3000',
});

Creating a remote HTTP interceptor

A remote interceptor is configured with type: 'remote'. The baseURL points to an interceptor server. Any request starting with the baseURL will be intercepted if a matching handler exists.

import { createHttpInterceptor } from '@zimic/interceptor/http';

interface User {
  username: string;
}

const interceptor = createHttpInterceptor<{
  '/users/:id': {
    GET: {
      response: {
        200: { body: User };
      };
    };
  };
}>({
  // The interceptor server is at http://localhost:4000
  // `/my-service` is a path to differentiate from other
  // interceptors using the same server
  type: 'remote',
  baseURL: 'http://localhost:4000/my-service',
});
Path discriminators in remote HTTP interceptors

A single interceptor server is perfectly capable of handling multiple interceptors and requests. Thus, additional paths are supported and might be necessary to differentiate between conflicting interceptors. If you may have multiple threads or processes applying mocks concurrently to the same interceptor server, it's important to keep the interceptor base URLs unique. Also, make sure that your application is considering the correct URL when making requests.

const interceptor = createHttpInterceptor<{
  // ...
}>({
  type: 'remote',
  // Declaring a base URL with a unique identifier to prevent conflicts
  baseURL: `http://localhost:4000/my-service-${crypto.randomUUID()}`,
});

// Your application should use this base URL when making requests
console.log(interceptor.baseURL);

Unhandled requests

When a request is not matched by any interceptor handlers, it is considered unhandled and will be logged to the console by default.

Tip

If you expected a request to be handled, but it was not, make sure that the interceptor base URL, path, method, and restrictions correctly match the request. Additionally, confirm that no errors occurred while creating the response.

In a local interceptor, unhandled requests can be either bypassed or rejected. Bypassed requests reach the real network, whereas rejected requests fail with an network error. The default behavior in local interceptors is to reject unhandled requests.

Remote interceptors and interceptor server always reject unhandled requests. This is because the unhandled requests have already reached the interceptor server, so there would be no way of bypassing them at this point.

You can override the logging behavior per interceptor with onUnhandledRequest in createHttpInterceptor(options) or by setting interceptor.onUnhandledRequest. onUnhandledRequest also accepts a function to dynamically determine which strategy to use for an unhandled request.

Example 1: Ignore unhandled requests in an interceptor without logging:
import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<Schema>({
  type: 'local',
  baseURL: 'http://localhost:3000',
  onUnhandledRequest: {
    action: 'bypass', // Allow unhandled requests to reach the real network
    log: false, // Do not log warnings about unhandled requests
  },
});
Example 2: Reject unhandled requests in an interceptor with logging:
import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<Schema>({
  type: 'local',
  baseURL: 'http://localhost:3000',
  onUnhandledRequest: {
    action: 'reject', // Do not allow unhandled requests to reach the real network
    log: true, // Log warnings about unhandled requests
  },
});
Example 3: Dynamically ignore or reject unhandled requests in an interceptor:
import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<Schema>({
  type: 'local',
  baseURL: 'http://localhost:3000',
  onUnhandledRequest: async (request) => {
    const url = new URL(request.url);

    // Ignore only unhandled requests to /assets
    if (url.pathname.startsWith('/assets')) {
      // Remember: 'bypass' is only available for local interceptors!
      // Use 'reject' for remote interceptors.
      return { action: 'bypass', log: false };
    }

    // Reject all other unhandled requests
    return { action: 'reject', log: true };
  },
});

Note

When a request is unhandled, Zimic looks for a running interceptor whose base URL is the prefix of the unhandled request URL. If such interceptor is found, its strategy is used, or the default strategy if none was defined. If multiple interceptors match the request URL, the last one started with await interceptor.start() will be used, regardless of existing another interceptor with a more specific base URL.

If no running interceptor matches the request, one of two things may happen:

  • If it was targeted to an interceptor server, it will be rejected with a network error. In this case, the logging behavior is configured with the --log-unhandled-requests option in the interceptor server.
  • If it was not targeted to an interceptor server, it will be bypassed and reach the real network.

Saving requests

The requestSaving option configures if the intercepted requests are saved and how they are handled. It supports the following properties:

Property Description Default
enabled Whether request handlers should save their intercepted requests in memory and make them accessible through handler.requests. If you are using interceptor.checkTimes() or handler.checkTimes(), consider enabling this option to get more detailed information in TimesCheckError errors. process.env.NODE_ENV === 'test'
safeLimit The safe number of requests to save in memory before logging warnings in the console. If requestSaving.enabled is true and the interceptor is not regularly cleared with interceptor.clear(), the requests may accumulate in memory and cause performance issues. This option does not limit the number of saved requests, only when to log warnings. 1000

Important

If requestSaving.enabled is true, make sure to regularly clear the interceptor to avoid requests accumulating in memory. A common practice is to call interceptor.clear() after each test.

See Testing for an example of how to manage the lifecycle of interceptors in your tests.

Using a local interceptor
import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<Schema>({
  type: 'local',
  baseURL: 'http://localhost:3000',
  requestSaving: { enabled: true, safeLimit: 1000 },
});

// Recommended: Clear the interceptor after each test.
// Use the equivalent of `afterEach` in your test framework.
afterEach(() => {
  interceptor.clear();
});
Using a remote interceptor
import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<Schema>({
  type: 'remote',
  baseURL: 'http://localhost:3000',
  requestSaving: { enabled: true, safeLimit: 1000 },
});

// Recommended: Clear the interceptor after each test.
// Use the equivalent of `afterEach` in your test framework.
afterEach(async () => {
  await interceptor.clear();
});

HTTP interceptor.start()

Starts the interceptor. Only interceptors that are running will intercept requests.

await interceptor.start();

When targeting a browser environment with a local interceptor, make sure to follow the client-side post-install guide before starting your interceptors.

HTTP interceptor.stop()

Stops the interceptor, preventing it from intercepting HTTP requests. Stopped interceptors are automatically cleared, exactly as if interceptor.clear() had been called.

await interceptor.stop();

HTTP interceptor.isRunning

Whether the interceptor is currently running and ready to use.

const isRunning = interceptor.isRunning;

HTTP interceptor.baseURL

The base URL of the interceptor.

console.log(interceptor.baseURL);

HTTP interceptor.platform

The platform used by the interceptor (browser or node).

console.log(interceptor.platform);

HTTP interceptor.<method>(path)

Creates an HttpRequestHandler for the given method and path. The path and method must be declared in the interceptor schema. The supported methods are: get, post, put, patch, delete, head, and options.

When using a remote interceptor, creating a handler is an asynchronous operation, so you need to await it. You can also chain any number of operations and apply them by awaiting the handler.

After a request is intercepted, Zimic tries to find a handler that matches it, considering the base URL of the interceptor, and the method, path, restrictions, and limits on the number of requests of the handler. The handlers are checked from the last one created to the first one, so new handlers have preference over older ones. This allows you to declare generic and specific handlers based on their order of creation. For example, a generic handler for GET /users can return an empty list, while a specific handler in a test case can return a list with some users. In this case, the specific handler will be considered first as long as it is created after the generic one.

Using a local interceptor
import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<{
  '/users': {
    GET: {
      response: {
        200: { body: User[] };
      };
    };
  };
}>({
  type: 'local',
  baseURL: 'http://localhost:3000',
});

const listHandler = interceptor.get('/users').respond({
  status: 200
  body: [{ username: 'me' }],
});
Using a remote interceptor
import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<{
  '/users': {
    GET: {
      response: {
        200: { body: User[] };
      };
    };
  };
}>({
  type: 'remote',
  baseURL: 'http://localhost:4000/my-service',
});

const listHandler = await interceptor.get('/users').respond({
  status: 200
  body: [{ username: 'me' }],
});

Path parameters

Paths with parameters are supported, such as /users/:id. Even when using a computed path (e.g. /users/1), the original path is automatically inferred, guaranteeing type safety.

import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<{
  '/users/:id': {
    PUT: {
      request: {
        body: { username: string };
      };
      response: {
        204: {};
      };
    };
  };
}>({
  type: 'local',
  baseURL: 'http://localhost:3000',
});

interceptor.get('/users/:id'); // Matches any id
interceptor.get(`/users/${1}`); // Only matches id 1

request.pathParams contains the parsed path parameters of a request and have their type automatically inferred from the path string. For example, the path /users/:userId will result in a request.pathParams of type { userId: string }.

Using a local interceptor
const updateHandler = interceptor.put('/users/:id').respond((request) => {
  console.log(request.pathParams); // { id: '1' }

  return {
    status: 200,
    body: { username: 'me' },
  };
});

await fetch('http://localhost:3000/users/1', { method: 'PUT' });
Using a remote interceptor
const updateHandler = await interceptor.put('/users/:id').respond((request) => {
  console.log(request.pathParams); // { id: '1' }

  return {
    status: 200,
    body: { username: 'me' },
  };
});

await fetch('http://localhost:3000/users/1', { method: 'PUT' });

HTTP interceptor.checkTimes()

Checks if all handlers created by this interceptor have matched the number of requests declared with handler.times().

If some handler has matched fewer or more requests than expected, this method will throw a TimesCheckError error, including a stack trace to the handler.times() that was not satisfied.

Tip

When requestSaving.enabled is true in your interceptor, the TimesCheckError errors will also list each unmatched request with diff of the expected and received data. This is useful for debugging requests that did not match a handler with restrictions.

Using a local interceptor
interceptor.checkTimes();
Using a remote interceptor
await interceptor.checkTimes();

This is useful in an afterEach hook (or equivalent) to make sure that all expected requests were made at the end of each test.

Using a local interceptor
afterEach(() => {
  interceptor.checkTimes();
});
Using a remote interceptor
afterEach(async () => {
  await interceptor.checkTimes();
});

See Testing for an example of how to manage the lifecycle of interceptors in your tests.

HTTP interceptor.clear()

Clears the interceptor and all of its HttpRequestHandler instances, including their registered responses and intercepted requests. After calling this method, the interceptor will no longer intercept any requests until new mock responses are registered.

This method is useful to reset the interceptor mocks between tests.

Using a local interceptor
interceptor.clear();
Using a remote interceptor
await interceptor.clear();

HttpInterceptor utility types

InferHttpInterceptorSchema

Infers the schema of an HTTP interceptor.

import { type InferHttpInterceptorSchema } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<{
  '/users': {
    GET: {
      response: { 200: { body: User[] } };
    };
  };
}>({
  type: 'local',
  baseURL: 'http://localhost:3000',
});

type Schema = InferHttpInterceptorSchema<typeof interceptor>;
// {
//   '/users': {
//     GET: {
//       response: { 200: { body: User[] } };
//     };
//   };
// }

HttpRequestHandler

HTTP request handlers allow declaring HTTP responses to return for intercepted requests. They also keep track of the intercepted requests and their responses, which can be used to check if the requests your application has made are correct.

When multiple handlers match the same method and path, the last created with interceptor.<method>(path) will be used.

HTTP handler.method

Returns the method that matches a handler.

Using a local interceptor
const handler = interceptor.post('/users');
console.log(handler.method); // 'POST'
Using a remote interceptor
const handler = await interceptor.post('/users');
console.log(handler.method); // 'POST'

HTTP handler.path

Returns the path that matches a handler. The base URL of the interceptor is not included, but it is used when matching requests.

Using a local interceptor
const handler = interceptor.get('/users');
console.log(handler.path); // '/users'
Using a remote interceptor
const handler = await interceptor.get('/users');
console.log(handler.path); // '/users'

HTTP handler.with(restriction)

Declares a restriction to intercepted requests. headers, searchParams, and body are supported to limit which requests will match the handler and receive the mock response. If multiple restrictions are declared, either in a single object or with multiple calls to handler.with(), all of them must be met, essentially creating an AND condition.

Static restrictions

Declaring restrictions for headers:
Using a local interceptor
const creationHandler = interceptor
  .get('/users')
  .with({
    headers: { authorization: `Bearer ${token}` },
  })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });
Using a remote interceptor
const creationHandler = await interceptor
  .get('/users')
  .with({
    headers: { authorization: `Bearer ${token}` },
  })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });

An equivalent alternative using HttpHeaders:

Using a local interceptor
import { type HttpSchema, HttpHeaders } from '@zimic/http';

type UserListHeaders = HttpSchema.Headers<{
  authorization: string;
}>;

const headers = new HttpHeaders<UserListHeaders>({
  authorization: `Bearer ${token}`,
});

const creationHandler = interceptor
  .get('/users')
  .with({ headers })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });
Using a remote interceptor
import { type HttpSchema, HttpHeaders } from '@zimic/http';

type UserListHeaders = HttpSchema.Headers<{
  authorization: string;
}>;

const headers = new HttpHeaders<UserListHeaders>({
  authorization: `Bearer ${token}`,
});

const creationHandler = await interceptor
  .get('/users')
  .with({ headers })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });
Declaring restrictions for search params:
Using a local interceptor
const creationHandler = interceptor
  .get('/users')
  .with({
    searchParams: { query: 'u' },
  })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });
Using a remote interceptor
const creationHandler = await interceptor
  .get('/users')
  .with({
    searchParams: { query: 'u' },
  })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });

An equivalent alternative using HttpSearchParams:

Using a local interceptor
import { type HttpSchema, HttpSearchParams } from '@zimic/http';

type UserListSearchParams = HttpSchema.SearchParams<{
  query?: string;
}>;

const searchParams = new HttpSearchParams<UserListSearchParams>({
  query: 'u',
});

const creationHandler = interceptor
  .get('/users')
  .with({ searchParams })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });
Using a remote interceptor
import { type HttpSchema, HttpSearchParams } from '@zimic/http';

type UserListSearchParams = HttpSchema.SearchParams<{
  query?: string;
}>;

const searchParams = new HttpSearchParams<UserListSearchParams>({
  query: 'u',
});

const creationHandler = await interceptor
  .get('/users')
  .with({ searchParams })
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  });
Declaring restrictions for a JSON body:
Using a local interceptor
const creationHandler = interceptor
  .post('/users')
  .with({
    body: { username: 'me' },
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });
Using a remote interceptor
const creationHandler = await interceptor
  .post('/users')
  .with({
    body: { username: 'me' },
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });

For JSON bodies to be correctly parsed, make sure that the intercepted requests have the header content-type: application/json.

Declaring restrictions for a form data body:
Using a local interceptor
import { type HttpSchema, HttpFormData } from '@zimic/http';

type UserCreationData = HttpSchema.FormData<{
  username: string;
  profilePicture: Blob;
}>;

const formData = new HttpFormData<UserCreationData>();
formData.append('username', 'me');
formData.append(
  'profilePicture',
  new File(['content'], 'profile.png', {
    type: 'image/png',
  }),
);

const creationHandler = interceptor
  .post('/users')
  .with({
    body: formData,
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });
Using a remote interceptor
import { type HttpSchema, HttpFormData } from '@zimic/http';

type UserCreationData = HttpSchema.FormData<{
  username: string;
  profilePicture: Blob;
}>;

const formData = new HttpFormData<UserCreationData>();
formData.append('username', 'me');
formData.append(
  'profilePicture',
  new File(['content'], 'profile.png', {
    type: 'image/png',
  }),
);

const creationHandler = await interceptor
  .post('/users')
  .with({
    body: formData,
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });

For form data bodies to be correctly parsed, make sure that the intercepted requests have the header content-type: multipart/form-data.

Declaring restrictions for a blob body:
Using a local interceptor
const creationHandler = interceptor
  .post('/users')
  .with({
    body: new Blob(['content'], {
      type: 'application/octet-stream',
    }),
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });
Using a remote interceptor
const creationHandler = await interceptor
  .post('/users')
  .with({
    body: new Blob(['content'], {
      type: 'application/octet-stream',
    }),
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });

For blob bodies to be correctly parsed, make sure that the intercepted requests have the header content-type indicating a binary data, such as application/octet-stream, image/png, audio/mp3, etc.

Declaring restrictions for a plain text body:
Using a local interceptor
const creationHandler = interceptor
  .post('/users')
  .with({
    body: 'content',
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });
Using a remote interceptor
const creationHandler = await interceptor
  .post('/users')
  .with({
    body: 'content',
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });
Declaring restrictions for search params (x-www-form-urlencoded) body:
Using a local interceptor
import { type HttpSchema, HttpSearchParams } from '@zimic/http';

type UserGetByIdSearchParams = HttpSchema.SearchParams<{
  username: string;
}>;

const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
  query: 'u',
});

const creationHandler = interceptor
  .post('/users')
  .with({
    body: searchParams,
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });
Using a remote interceptor
import { type HttpSchema, HttpSearchParams } from '@zimic/http';

type UserGetByIdSearchParams = HttpSchema.SearchParams<{
  username: string;
}>;

const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
  query: 'u',
});

const creationHandler = await interceptor
  .post('/users')
  .with({
    body: searchParams,
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });

For plain text bodies to be correctly parsed, make sure that the intercepted requests have the header content-type indicating a plain text, such as text/plain.

By default, restrictions use exact: false, meaning that any request containing the declared restrictions will match the handler, regardless of having more properties or values. In the examples above, requests with more properties in the headers, search params, or body would still match the restrictions.

If you want to match only requests with the exact values declared, you can use exact: true:

Using a local interceptor
const creationHandler = interceptor
  .post('/users')
  .with({
    headers: { 'content-type': 'application/json' },
    body: { username: 'me' },
    exact: true, // Only requests with these exact headers and body will match
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });
Using a remote interceptor
const creationHandler = await interceptor
  .post('/users')
  .with({
    headers: { 'content-type': 'application/json' },
    body: { username: 'me' },
    exact: true, // Only requests with these exact headers and body will match
  })
  .respond({
    status: 201,
    body: { username: 'me' },
  });

Computed restrictions

A function is also supported to declare restrictions in case they are dynamic. Learn more about the request object at Intercepted HTTP resources.

Using a local interceptor
const creationHandler = interceptor
  .post('/users')
  .with((request) => {
    const accept = request.headers.get('accept');
    return accept !== null && accept.startsWith('application');
  })
  .respond({
    status: 201,
    body: [{ username: 'me' }],
  });
Using a remote interceptor
const creationHandler = await interceptor
  .post('/users')
  .with((request) => {
    const accept = request.headers.get('accept');
    return accept !== null && accept.startsWith('application');
  })
  .respond({
    status: 201,
    body: [{ username: 'me' }],
  });

The function should return a boolean: true if the request matches the handler and should receive the mock response; false otherwise.

HTTP handler.respond(declaration)

Declares a response to return for matched intercepted requests.

When the handler matches a request, it will respond with the given declaration. The response type is statically validated against the schema of the interceptor.

Static responses

Declaring responses with JSON body:
Using a local interceptor
const listHandler = interceptor.get('/users').respond({
  status: 200,
  body: [{ username: 'me' }],
});
Using a remote interceptor
const listHandler = await interceptor.get('/users').respond({
  status: 200,
  body: [{ username: 'me' }],
});
Declaring responses with form data body:
Using a local interceptor
import { type HttpSchema, HttpFormData } from '@zimic/http';

type UserGetByIdData = HttpSchema.FormData<{
  username: string;
  profilePicture: Blob;
}>;

const formData = new HttpFormData<UserGetByIdData>();
formData.append('username', 'me');
formData.append(
  'profilePicture',
  new File(['content'], 'profile.png', {
    type: 'image/png',
  }),
);

const listHandler = interceptor.get('/users/:id').respond({
  status: 200,
  body: formData,
});
Using a remote interceptor
import { type HttpSchema, HttpFormData } from '@zimic/http';

type UserGetByIdData = HttpSchema.FormData<{
  username: string;
  profilePicture: Blob;
}>;

const formData = new HttpFormData<UserGetByIdData>();
formData.append('username', 'me');
formData.append(
  'profilePicture',
  new File(['content'], 'profile.png', {
    type: 'image/png',
  }),
);

const listHandler = await interceptor.get('/users/:id').respond({
  status: 200,
  body: formData,
});
Declaring responses with blob body:
Using a local interceptor
const listHandler = interceptor.get('/users').respond({
  status: 200,
  body: new Blob(['content'], {
    type: 'application/octet-stream',
  }),
});
Using a remote interceptor
const listHandler = await interceptor.get('/users').respond({
  status: 200,
  body: new Blob(['content'], {
    type: 'application/octet-stream',
  }),
});
Declaring responses with plain text body:
Using a local interceptor
const listHandler = interceptor.get('/users').respond({
  status: 200,
  body: 'content',
});
Using a remote interceptor
const listHandler = await interceptor.get('/users').respond({
  status: 200,
  body: 'content',
});
Declaring responses with search params (x-www-form-urlencoded) body:
Using a local interceptor
import { type HttpSchema, HttpSearchParams } from '@zimic/http';

type UserGetByIdSearchParams = HttpSchema.SearchParams<{
  username: string;
}>;

const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
  query: 'u',
});

const listHandler = interceptor.get('/users').respond({
  status: 200,
  body: searchParams,
});
Using a remote interceptor
import { type HttpSchema, HttpSearchParams } from '@zimic/http';

type UserGetByIdSearchParams = HttpSchema.SearchParams<{
  username: string;
}>;

const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
  query: 'u',
});

const listHandler = await interceptor.get('/users').respond({
  status: 200,
  body: searchParams,
});

Computed responses

A function is also supported to declare a response in case it is dynamic. Learn more about the request object at Intercepted HTTP resources.

Using a local interceptor
const listHandler = interceptor.get('/users').respond((request) => {
  const username = request.searchParams.get('username');

  if (!username) {
    return { status: 400 };
  }

  return {
    status: 200,
    body: [{ username }],
  };
});
Using a remote interceptor
const listHandler = await interceptor.get('/users').respond((request) => {
  const username = request.searchParams.get('username');

  if (!username) {
    return { status: 400 };
  }

  return {
    status: 200,
    body: [{ username }],
  };
});

HTTP handler.times()

Declares a number of intercepted requests that the handler will be able to match and return its response.

If only one argument is provided, the handler will match exactly that number of requests. In case of two arguments, the handler will consider an inclusive range, matching at least the minimum (first argument) and at most the maximum (second argument) number of requests.

Once the handler receives more requests than the maximum number declared, it will stop matching requests and returning its response. In this case, Zimic will try other handlers until one eligible is found, otherwise the request will be either bypassed or rejected. Learn more about how Zimic decides which handler to use for an intercepted request in the interceptor.<method>(path) API reference.

Using a local interceptor
const exactListHandler = interceptor
  .get('/users')
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  })
  .times(1); // Matches exactly one request

const rangeListHandler = interceptor
  .get('/users')
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  })
  .times(0, 3); // Matches at least 0 and at most 3 requests
Using a remote interceptor
const exactListHandler = await interceptor
  .get('/users')
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  })
  .times(1); // Matches exactly one request

const rangeListHandler = await interceptor
  .get('/users')
  .respond({
    status: 200,
    body: [{ username: 'me' }],
  })
  .times(0, 3); // Matches at least 0 and at most 3 requests

Important

To make sure that all expected requests were made, use interceptor.checkTimes() or handler.checkTimes(). interceptor.checkTimes() is generally preferred, as it checks all handlers created by the interceptor with a single call.

Tip

Prior to v0.12.0, a common strategy to check the number of requests was to assert the length of handler.requests. handler.times(), combined with handler.checkTimes() or interceptor.checkTimes(), archives the same purpose in a shorter and more declarative way. In most cases, these methods are preferred over manually checking the length of handler.requests.

HTTP handler.checkTimes()

Checks if the handler has matched the expected number of requests declared with handler.times().

If the handler has matched fewer or more requests than expected, this method will throw a TimesCheckError error, including a stack trace to the handler.times() that was not satisfied.

Using a local interceptor
const listHandler = interceptor
  .get('/users')
  .respond({
    status: 200,
    body: [],
  })
  .times(1);

// Run application...

// Check that exactly 1 request was made
handler.checkTimes();
Using a remote interceptor
const listHandler = await interceptor
  .get('/users')
  .respond({
    status: 200,
    body: [],
  })
  .times(1);

// Run application...

// Check that exactly 1 request was made
await handler.checkTimes();

HTTP handler.clear()

Clears any response declared with handler.respond(declaration), restrictions declared with handler.with(restriction), and intercepted requests, making the handler stop matching requests. The next handler, created before this one, that matches the same method and path will be used if present. If not, the requests of the method and path will not be intercepted.

To make the handler match requests again, register a new response with handler.respond().

Using a local interceptor
const genericHandler = interceptor.get('/users').respond({
  status: 200,
  body: [],
});

const specificHandler = interceptor.get('/users').respond({
  status: 200,
  body: [{ username: 'me' }],
});

specificHandler.clear();
// Now, requests GET /users will match `genericHandler` and receive an empty array

console.log(specificHandler.requests); // []
Using a remote interceptor
const genericHandler = await interceptor.get('/users').respond({
  status: 200,
  body: [],
});

const specificHandler = await interceptor.get('/users').respond({
  status: 200,
  body: [{ username: 'me' }],
});

await specificHandler.clear();
// Now, requests GET /users will match `genericHandler` and receive an empty array

console.log(specificHandler.requests); // []

HTTP handler.requests

Returns the intercepted requests that matched this handler, along with the responses returned to each of them. This is useful for testing that the correct requests were made by your application. Learn more about the request and response objects at Intercepted HTTP resources.

Important

This method can only be used if requestSaving.enabled is true when creating the interceptor. See Saving intercepted requests for more information.

Using a local interceptor
const updateHandler = interceptor.put('/users/:id').respond((request) => {
  const newUsername = request.body.username;
  return {
    status: 200,
    body: [{ username: newUsername }],
  };
});

await fetch(`http://localhost:3000/users/${1}`, {
  method: 'PUT',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ username: 'new' }),
});

expect(updateHandler.requests).toHaveLength(1);
expect(updateHandler.requests[0].pathParams).toEqual({ id: '1' });
expect(updateHandler.requests[0].body).toEqual({ username: 'new' });
expect(updateHandler.requests[0].response.status).toBe(200);
expect(updateHandler.requests[0].response.body).toEqual([{ username: 'new' }]);
Using a remote interceptor
const updateHandler = await interceptor.put('/users/:id').respond((request) => {
  const newUsername = request.body.username;
  return {
    status: 200,
    body: [{ username: newUsername }],
  };
});

await fetch(`http://localhost:3000/users/${1}`, {
  method: 'PUT',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ username: 'new' }),
});

expect(updateHandler.requests).toHaveLength(1);
expect(updateHandler.requests[0].pathParams).toEqual({ id: '1' });
expect(updateHandler.requests[0].body).toEqual({ username: 'new' });
expect(updateHandler.requests[0].response.status).toBe(200);
expect(updateHandler.requests[0].response.body).toEqual([{ username: 'new' }]);

Intercepted HTTP resources

The intercepted requests and responses are typed based on their interceptor schema. They are available as simplified objects based on the Request and Response web APIs. body contains the parsed body, while typed headers, path params and search params are in headers, pathParams, and searchParams, respectively.

The body is automatically parsed based on the header content-type of the request or response. The following table shows how each type is parsed, where * indicates any other resource that does not match the previous types:

content-type Parsed to
application/json JSON
application/xml String
application/x-www-form-urlencoded HttpSearchParams
application/* (others) Blob
multipart/form-data HttpFormData
multipart/* (others) Blob
text/* String
image/* Blob
audio/* Blob
font/* Blob
video/* Blob
*/* (others) JSON if possible, otherwise String

If no content-type exists or it is unknown, Zimic tries to parse the body as JSON and falls back to plain text if it fails.

If you need access to the original Request and Response objects, you can use the request.raw property:

console.log(request.raw); // Request{}
console.log(request.response.raw); // Response{}

Guides

Clone this wiki locally