@fastify/reply-from

repository·main·Indexed 41 days ago

https://github.com/fastify/fastify-reply-from

A Fastify plugin that allows you to forward the current HTTP request to another server, acting as a proxy. It supports HTTP/1.1 via undici or native Node.js http, as well as HTTP/2 to HTTP forwarding. Version 12.6.5 provides capabilities to configure base URLs, load-balance requests using undici.BalancedPool, and customize request/response headers and bodies via the reply.from() method.

Tokens
4.4K
Snippets
12
Records
14
Agent score
1%

What's inside @fastify/reply-from

  1. Compatibility with @fastify/multipart

    main

    When using @fastify/reply-from alongside @fastify/multipart, avoid registering them as sibling plugins or within a parent-child plugin relationship.

    Because @fastify/multipart parses and consumes multipart content, that content will not be available for @fastify/reply-from to forward to the target service if they are in the same plugin branch. To use both safely, ensure they belong to disjoint branches of the Fastify plugin hierarchy tree.

  2. Forward HTTP requests using @fastify/reply-from

    main

    To use this plugin, register it with your Fastify instance and provide a base URL in the options. You can then use the reply.from(source, [opts]) method within a route handler to proxy the current request to the target server. This plugin supports forwarding HTTP/2 requests to HTTP servers.

    'use strict'
    
    const Fastify = require('fastify')
    
    const target = Fastify({
      logger: true
    })
    
    target.get('/', (request, reply) => {
      reply.send('hello world')
    })
    
    const proxy = Fastify({
      logger: true
    })
    
    // Register the plugin with a base URL
    proxy.register(require('@fastify/reply-from'), {
      base: 'http://localhost:3001/'
    })
    
    // Use reply.from() to forward the request
    proxy.get('/', (request, reply) => {
      reply.from('/')
    })
    
    target.listen({ port: 3001 }, (err) => {
      if (err) {
        throw err
      }
    
      proxy.listen({ port: 3000 }, (err) => {
        if (err) {
          throw err
        }
      })
    })
  3. Proxy specific content types without parsing

    main

    If you need to proxy content types like multipart/form-data or text/event-stream using @fastify/reply-from, you must prevent Fastify from parsing them. If the body is parsed, it is consumed and cannot be forwarded to the upstream server.

    You can achieve this by registering custom content type parsers that pass the raw body through using fastify.addContentTypeParser.

    // Register custom content type parsers that pass raw body through
    fastify.addContentTypeParser('multipart/form-data', function (req, body, done) {
      done(null, body)
    })
    
    fastify.addContentTypeParser('text/event-stream', function (req, body, done) {
      done(null, body)
    })
    
    fastify.register(require('@fastify/reply-from'))
    
    fastify.post('/upload', (request, reply) => {
      // The multipart data will be proxied as-is to the upstream server
      reply.from('http://upstream-server.com/upload')
    })
    
    fastify.post('/events', (request, reply) => {
      // The SSE data will be proxied as-is to the upstream server
      reply.from('http://upstream-server.com/events')
    })
  4. Configure the `undici` client

    main

    By default, the plugin uses undici for HTTP/1.1 requests, which can provide 20-50% more throughput. You can customize undici settings or provide a custom instance.

    Options include:

    • connections: Number of connections.
    • pipelining: Pipelining count.
    • keepAliveTimeout: Keep-alive timeout in ms.
    • tls: TLS configuration (e.g., rejectUnauthorized).
    • proxy: A proxy URL for the undici client.

    You can also pass a custom undici.Pool instance directly to the undici key.

    proxy.register(require('@fastify/reply-from'), {
      base: 'http://localhost:3001/',
      undici: {
        connections: 128,
        pipelining: 1,
        keepAliveTimeout: 60 * 1000,
        tls: {
          rejectUnauthorized: false
        },
        proxy: 'http://my.proxy.server:8080',
      }
    })
  5. Configure the `http` client (Node.js native)

    main

    If http2 is not enabled, you can use Node's native http.request. You can customize the request by providing agentOptions (for http.Agent) and requestOptions (for the request itself). Alternatively, you can pass custom HTTP agents directly via the agents object mapping protocols (http: and https:) to agent instances.

    proxy.register(require('@fastify/reply-from'), {
      base: 'http://localhost:3001/',
      http: {
        agentOptions: {
          keepAliveMsecs: 10 * 60 * 1000
        },
        requestOptions: {
          timeout: 5000
        }
      }
    })
  6. How reply.from() handles request bodies and encoding

    main

    The reply.from() method automatically handles body forwarding and JSON encoding based on the following logic:

    1. Manual Body: If opts.body is provided:

      • If opts.body is a stream, an error is thrown (streams are not yet supported as a new body).
      • If opts.contentType is not provided, it defaults to application/json and the body is stringified via JSON.stringify.
      • If opts.body is null, the body is removed and content-length is set to 0.
    2. Automatic Body: If opts.body is not provided, it uses this.request.body:

      • If this.request.body is a Stream, it is forwarded as-is.
      • If it is not a stream, the plugin checks the Content-Type of the incoming request. If the type is in the contentTypesToEncode set (defaulting to application/json), the body is transparently stringified via JSON.stringify before being sent upstream.

    Note: For GET or HEAD requests, providing a body is considered a programmer error and will throw an error.

  7. Configure `http2` settings

    main

    To connect to an HTTP/2 server, set http2 to true or provide a settings object. The settings object allows control over session and request timeouts, as well as session and request options.

    • sessionTimeout: HTTP/2 session timeout in ms (default: 60000).
    • requestTimeout: HTTP/2 request timeout in ms (default: 10000).
    • sessionOptions: Options for http2.connect.
    • requestOptions: Options for the HTTP/2 request.
    proxy.register(require('@fastify/reply-from'), {
      base: 'http://localhost:3001/',
      http2: {
        sessionTimeout: 10000,
        requestTimeout: 5000,
        sessionOptions: {
          rejectUnauthorized: true
        },
        requestOptions: {
          endStream: true
        }
      }
    })
  8. Configure the `base` URL for forwarded requests

    main

    The base option sets the base URL for all forwarded requests. If http2 is set to true, base is required. Note that any path provided in the base string will be discarded; only the origin is used.

    • Single string: Uses a standard undici.Pool or http.request client.
    • Array (≥ 2 elements): Automatically uses undici.BalancedPool to load-balance requests round-robin across the provided origins. Only the origin (protocol://host:port) is considered.

    To forward requests to a Unix socket, use the unix+http: or unix+https: protocols by escaping the socket path as the hostname.

    const socketPath = require('node:querystring').escape('/run/http-daemon.socket')
    proxy.register(require('@fastify/reply-from'), {
      base: 'unix+http://${socketPath}/'
    });
  9. Register the @fastify/reply-from plugin

    main

    To use the plugin, register it with your Fastify instance. You can configure how the proxy behaves using the opts object.

    Plugin Options

    OptionTypeDefaultDescription
    basestringundefinedThe base URL to which requests will be forwarded.
    contentTypesToEncodeArray<string>['application/json']Additional content types that should be transparently JSON-encoded.
    retryMethodsArray<string>['GET', 'HEAD', 'OPTIONS', 'TRACE']HTTP methods that are eligible for automatic retries.
    disableCachebooleanfalseIf true, disables the internal LRU cache used for URL parsing.
    cacheURLsnumber100The maximum number of URLs to store in the LRU cache.
    httpbooleanundefinedEnable/disable HTTP/1.1.
    http2booleanundefinedEnable/disable HTTP/2.
    sessionTimeoutnumberundefinedSession timeout for the request.
    undiciobjectundefinedUndici specific options.
    globalAgentobjectundefinedGlobal agent for requests.
    destroyAgentbooleanundefinedWhether to destroy the agent on plugin close.
    disableRequestLoggingbooleanfalseIf true, disables request logging.
    retryDelayfunctionundefinedA custom function to determine retry delays.
    const fastify = require('fastify')
    const fastifyReplyFrom = require('@fastify/reply-from')
    
    const app = fastify()
    
    app.register(fastifyReplyFrom, {
      base: 'https://api.example.com'
    })
  10. Configure `retryDelay` custom logic

    main

    You can provide a custom handler to retryDelay to define how the client should wait between retries. This allows you to implement custom logic for specific error codes or status codes.

    The handler receives an object containing:

    • err: The error thrown by the agent.
    • req: The low-level request object.
    • res: The low-level response object (can be null).
    • attempt: The current retry attempt number.
    • getDefaultRetry: A callback to invoke the default retry logic.
    • retriesCount: The number of retries configured by the client.

    If you return a value from the handler, that is used as the delay. If you return null, the retry logic for that specific case is bypassed (unless handled by getDefaultRetry).

    const customRetryLogic = ({err, req, res, attempt, getDefaultRetry}) => {
      const defaultDelay = getDefaultRetry(req, res, err, attempt);
      if (defaultDelay) return defaultDelay();
    
      if (res && res.statusCode === 500 && req.method === 'GET') {
        return 300;
      }
    
      if (err && err.code == "UND_ERR_SOCKET"){
        return 600;
      }
    
      return null;
    }
    
    fastify.register(FastifyReplyFrom, {
      base: 'http://localhost:3001/',
      retryDelay: customRetryLogic
    })
  11. Use `reply.from(source, [opts])` to proxy requests

    main

    The from method decorates the Fastify Reply instance. It allows you to reply to the original request by forwarding it to a source URL.

    Note: If base was specified in the plugin registration, the source should not override the host/origin.

    Available opts for reply.from():

    • onResponse(request, reply, response): Called when a response is received. Use this to modify the body or remove headers like content-length if the body size changes. Note: headers are already sent at this stage.
    • onError(reply, error): Called on error. You must reply with the error.
    • rewriteHeaders(headers, request): Rewrites response headers before they are copied to the outer response.
    • rewriteRequestHeaders(request, headers): Rewrites request headers before they are sent to the source.
    • getUpstream(request, base): Dynamically determines the upstream destination (HTTP/1 only).
    • queryString: Replaces the original query string. Can be an object or a function returning a string.
    • body: Replaces the request body. Setting to null strips the body and content-type. (Not allowed for GET/HEAD).
    • method: Replaces the request method.
    • retriesCount: Number of times to retry on ECONNRESET.
    • contentType: Overrides the Content-Type header if body is being overridden.
    • timeout: Specific timeout for this request, overriding plugin defaults.
    // Example: Modifying the response body in onResponse
    reply.from('http://upstream-service/api', {
      onResponse: (request, reply, res) => {
        reply.removeHeader('content-length');
        reply.send('New body of different length');
      }
    })