@mswjs/interceptors

repository·main·Indexed 20 days ago

https://github.com/mswjs/interceptors

A low-level network interception library for Node.js that operates at the TCP/TLS handle level. It provides a foundation for building higher-level API mocking tools by intercepting raw TCP/TLS socket connections, HTTP requests (regardless of client), Fetch requests, and WebSocket connections. It includes specialized interceptors such as SocketInterceptor, HttpRequestInterceptor, FetchInterceptor, and WebSocketInterceptor, as well as a BatchInterceptor for combining multiple interceptors.

Tokens
14.6K
Snippets
46
Records
62
Agent score
71%

What's inside @mswjs/interceptors

  1. What is @mswjs/interceptors and when to use it?

    main

    @mswjs/interceptors is a low-level network interception library for Node.js.

    Important: This is not an API mocking library. It is a foundational tool designed to help developers build higher-level API mocking libraries (like Nock or Mock Service Worker) by providing a unified, compliant network interception algorithm.

    Use this library if you need to intercept:

    • Raw TCP and TLS socket connections (net.connect(), tls.connect())
    • HTTP requests regardless of the client (e.g., http.request(), axios(), etc.)
    • Fetch requests (global fetch() or custom implementations like undici())
    • WebSocket connections (global WebSocket constructor)
  2. Understand Undici's connection lifecycle for interception

    main

    When using Undici's fetch, the HTTP message is written to the socket only after the socket emits the connect event. This differs from the standard http.request() behavior, where the HTTP message is written immediately and buffered until the connect event occurs.

    Because Undici's connect callback is handled internally within its dispatch logic (specifically within Client.connect()), it is not passed as a standard connection callback to net.connect(). This makes the connection event invisible to standard interceptors that rely on intercepting the connection callback.

  3. How Interceptors work at the TCP/TLS level

    main

    Unlike traditional interception methods that patch http.request() (which often short-circuits the network code and creates a 'black box'), Interceptors implement interception at the TCP/TLS handle level.

    This approach executes as much of the Node.js network code as possible, even when mocking requests. The algorithm uses a multi-layered strategy:

    1. Socket Level: Spies on Socket.prototype.connect, net.connect(), and tls.connect().
    2. Stubbing: Stubs TCPWrap/TLSWrap until connections are claimed or passed through.
    3. Higher-level Interceptors: Wraps socket interception in interceptors like HttpRequestInterceptor to pipe packets through parsers.
    4. Request Client Interceptors: Uses AsyncLocalStorage to annotate request initiators without intercepting traffic directly.

    This minimizes deviations from normal system behavior and provides a more compliant mocking experience.

  4. Handle Node.js CONNECT requests

    main

    Although forbidden by the Fetch API specification, Node.js supports CONNECT requests. These requests have unique behaviors:

    • They are sent to the running server (options.host + options.port) to notify it of the intent to connect elsewhere.
    • They do not establish new connections; instead, the server manages the existing socket instance to establish the connection.
    • The response event is never emitted.

    To handle these, you must listen for the connect event on the client.

    const client = http.request({
      // Actual server handling the "CONNECT" request.
      host: '127.0.0.1',
      port: 1337,
      // The (proxy) authority in a "host:port" format.
      path: 'www.example.com:80'
    })
    
    client.on('connect', (request, socket) => {
      // Use "socket" to write to the authority...
    })
  5. Choose the right Interceptor for your needs

    main

    The @mswjs/interceptors library provides several specialized interceptors to spy on different request-issuing modules. Choose the one that matches the layer or module you want to intercept:

    • SocketInterceptor: The lowest level. Intercepts every outgoing TCP and TLS connection in Node.js at the net.Socket level.
    • HttpRequestInterceptor: Intercepts all HTTP requests in Node.js, regardless of the client (includes http/https, fetch, Undici, Axios, Got, etc.).
    • ClientRequestInterceptor: Specifically spies on http.ClientRequest (http.get/http.request).
    • XMLHttpRequestInterceptor: Spies on XMLHttpRequest (works in both browser and Node.js/JSDOM).
    • FetchInterceptor: Spies on the global fetch function.
    • WebSocketInterceptor: Spies on WebSocket connections created via the global WHATWG WebSocket class.

    You can combine multiple interceptors using BatchInterceptor.

  6. Understand TLSWrap and TLS socket connection behavior

    main

    TLS sockets extend net.Socket but behave differently regarding connection finalization. While regular net.Socket connections rely on the afterConnect callback of a TCPWrap request, TLS sockets wrap the net.Socket._handle in a TLSWrap.

    Key characteristics of TLSWrap:

    • It is responsible for the connection.
    • It calls the afterConnect callback internally in C++ code.
    • There is no public method to trigger afterConnect manually.
    • It provides its own specific methods, such as onhandshakedone and verifyError.
  7. Use environment presets with BatchInterceptor

    main

    Instead of manually listing interceptors, you can use pre-defined presets to capture all requests for a specific environment within a BatchInterceptor.

    Node.js preset

    Combines ClientRequestInterceptor, XMLHttpRequestInterceptor, and FetchInterceptor.

    import { BatchInterceptor } from '@mswjs/interceptors'
    import nodeInterceptors from '@mswjs/interceptors/presets/node'
    
    const interceptor = new BatchInterceptor({
      name: 'my-interceptor',
      interceptors: nodeInterceptors,
    })
    
    interceptor.on('request', listener)
    
    interceptor.apply()

    Browser preset

    Combines XMLHttpRequestInterceptor and FetchInterceptor.

    import { BatchInterceptor } from '@mswjs/interceptors'
    import browserInterceptors from '@mswjs/interceptors/presets/browser'
    
    const interceptor = new BatchInterceptor({
      name: 'my-interceptor',
      interceptors: browserInterceptors,
    })
    
    interceptor.on('request', listener)
    
    interceptor.apply()
  8. Use `HttpRequestInterceptor` to observe and mock HTTP requests

    main

    The HttpRequestInterceptor intercepts all HTTP requests in Node.js. It exposes requests as Fetch API Request instances.

    Observing Requests

    Use the request event to inspect requests. To read the body, you must use request.clone().json() (or similar) to avoid consuming the stream.

    Request Initiator

    The initiator property tells you which client issued the request. To accurately identify the client (e.g., distinguishing fetch from http.ClientRequest), you should apply the corresponding client-level interceptor (like FetchInterceptor) alongside the HttpRequestInterceptor.

    Modifying Requests

    You can mutate headers on the request object within the listener. Note that the request representation is read-only for other properties; it is not intended as a full-scale proxy.

    Mocking Responses

    Use controller.respondWith(new Response(...)) to mock a response. This must be done within the same tick as the listener. For asynchronous side-effects, make the listener an async function and await them.

    Mocking Errors

    • Generic Network Error: Use controller.respondWith(Response.error()).
    • Specific Error: Use controller.errorWith(new Error('reason')) to provide a custom error reason.
    import { HttpRequestInterceptor } from '@mswjs/interceptors/http'
    
    const interceptor = new HttpRequestInterceptor()
    interceptor.apply()
    
    // Observing and Mocking
    interceptor.on('request', async ({ request, controller }) => {
      // 1. Observe
      console.log(request.method, request.url)
    
      // 2. Modify headers
      request.headers.set('x-my-header', 'true')
    
      // 3. Mock a response (async example)
      await new Promise(resolve => setTimeout(resolve, 100))
      controller.respondWith(new Response(JSON.stringify({ hello: 'world' }), { status: 200 }))
    })
    
    // Observing responses
    interceptor.on('response', ({ response, responseType }) => {
      // responseType is 'mock' if responded via controller, 'original' otherwise
      console.log(responseType)
    })
  9. How SocketInterceptor handles DNS lookups

    main

    To ensure that interception logic triggers even for non-existent hosts and to avoid real DNS resolution during the initial interception phase, SocketInterceptor uses a mockLookup function.

    This function:

    1. Always succeeds.
    2. Resolves any hostname to the loopback address (127.0.0.1 for IPv4 or ::1 for IPv6).
    3. Executes asynchronously via process.nextTick to ensure that consumers have time to attach listeners to the socket before the lookup callback is invoked.
    4. Honors the Node.js lookup contract regarding the all option (returning an array of addresses if all is true, or a single address otherwise).
  10. WebSocketInterceptor connection event structure

    main

    When WebSocketInterceptor intercepts a connection, it emits a connection event. This event provides access to the two sides of the mocked connection:

    • client: A WebSocketClientConnection instance representing the client-side view of the socket.
    • server: A WebSocketServerConnection instance representing the server-side view, allowing you to call .send() to push data to the client or .connect() to attempt a real connection.
    • info: Contains connection metadata, such as protocols.
  11. How WebSocketServerConnection handles event forwarding and cancellation

    main

    WebSocketServerConnection acts as a bridge between the real server and the intercepted client. It implements specific logic for forwarding events while allowing for interception:

    Incoming Messages

    When the real server sends a message, WebSocketServerConnection dispatches a CancelableMessageEvent named message.

    • If the listener calls event.preventDefault(), the message is not forwarded to the mock client.
    • If not prevented, the message is forwarded to the client as a standard MessageEvent.

    Server Errors

    When the real server emits an error event, it is dispatched to the server connection first.

    • If the error event is prevented, it is not forwarded to the client.
    • If not prevented, the error is forwarded to the client.

    Server Closures

    When the real server closes the connection, a CancelableCloseEvent named close is dispatched.

    • If the closure is prevented, the mock client is not notified of the closure.
    • If not prevented, the mock client is forcefully closed using the server's provided code and reason to ensure the client reflects the server's state accurately.