最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - How do I fix my issue with a Dynamic Reverse Proxy and POST requests? - Stack Overflow

programmeradmin3浏览0评论

So, I am trying to setup a system that allows traffic to be redirected to a docker container based on a project ID stored in a database. I use Nginx as a reverse proxy which proxies to a fastify reverse proxy, this may not be super efficient but seems to work for GET requests but POST requests end up timing out. I am doubtful that it is the docker container it is proxying the request to that is causing the error as the logs of that docker container for a post request seem to not output anything.
Here is my code for the dynamic reverse proxy:

import Fastify, { FastifyRequest } from 'fastify';
import { db } from './db';
import { routingTable } from './db/schema';
import { eq } from 'drizzle-orm';
import dotenv from 'dotenv';
import Docker from 'dockerode';
import httpProxy from 'http-proxy';
import crypto from 'crypto';
import fastifyFormBody from '@fastify/formbody';
import fastifyMultipart from '@fastify/multipart';

dotenv.config();

const fastify = Fastify({
  logger: true,
  // Increase body size limits for proxying larger POST requests
  bodyLimit: 30 * 1024 * 1024 // 30MB
});

// Register fastify body parser plugins to handle different content types
fastify.register(fastifyFormBody);
fastify.register(fastifyMultipart, {
  limits: {
    fileSize: 25 * 1024 * 1024 // 25MB limit for file uploads
  }
});

// Create a proxy server instance
const proxy = httpProxy.createProxyServer({
  changeOrigin: true,
  xfwd: true, // Add X-Forwarded headers
  proxyTimeout: 120000, // Increase timeout to 2 minutes
  buffer: undefined, // Let the proxy handle buffering based on the request
});

// Add special handling for the proxy 
proxy.on('proxyReq', (proxyReq, req, res, options) => {
  // Ensure content-length is preserved for requests with bodies
  if (req.headers['content-length']) {
    proxyReq.setHeader('Content-Length', req.headers['content-length']);
  }
  
  // Ensure content-type is preserved 
  if (req.headers['content-type']) {
    proxyReq.setHeader('Content-Type', req.headers['content-type']);
  }
});

// Add response handling to manage incoming responses
proxy.on('proxyRes', (proxyRes, req, res) => {
  fastify.log.info(`Received proxy response from target with status: ${proxyRes.statusCode}`);
});

// Handle proxy errors
// @ts-ignore - Ignore TypeScript errors for now as the http-proxy types are hard to match exactly
proxy.on('error', function(err: any, req: any, res: any) {
  if (res.writeHead) {
    fastify.log.error(`Proxy error: ${err.message}`);
    res.writeHead(500, { 'Content-Type': 'text/plain' });
    res.end('Proxy error: ' + err.message);
  }
});
const docker = new Docker({ socketPath: '/var/run/docker.sock' });

// Define a route for handling all requests
fastify.all('*', async (request, reply) => {
  const host = request.headers.host;
  if (!host) {
    return reply.status(400).send({ error: "Host header is required" });
  }
  
  const hostParts = host.split('.');
  let subdomain = hostParts.at(0) ?? null;
  if (!subdomain) {
    return reply.status(400).send({ error: "Invalid host format" });
  }
  const useActionRunner = subdomain.includes("action");
  if (useActionRunner) {
    subdomain = subdomain.replace('-action', '');
  }
  
  // If the subdomain is "server", allow access to server endpoints directly
  if (subdomain.toLowerCase() === 'server') {
    // Skip proxying and let the request continue to other routes
    return;
  }
  
  // Query PostgreSQL for the port
  const result = await db
    .select()
    .from(routingTable)
    .where(eq(routingTable.projectId, subdomain));

  if (result.length === 0) {
    return reply.status(404).send({ error: "Project not found" });
  }

  // Use http-proxy instead of reply.from()
  const targetUrl = useActionRunner ? `http://localhost:${result[0].action_port}` : `http://localhost:${result[0].host_port}`;
  
  // Set up a custom completion handler
  const proxyHandler = (callback: () => void) => {
    // Mark the response as handled
    reply.hijack();
    
    const options: httpProxy.ServerOptions = {
      target: targetUrl,
      ignorePath: false,
      prependPath: false,
      selfHandleResponse: false,
      timeout: 120000 // Match the main proxy timeout
    };
    
    // Handle different types of requests
    if (request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH') {
      // For requests with bodies, ensure the content-type and body are preserved
      fastify.log.info(`Proxying ${request.method} request to ${targetUrl}${request.url} with Content-Type: ${request.headers['content-type']}, Content-Length: ${request.headers['content-length']}`);
      
      // Add special handling for POST requests to ensure the body gets forwarded properly
      const startTime = Date.now();
      
      // Set a longer timeout for the request to complete
      const reqTimeout = setTimeout(() => {
        fastify.log.error(`Request to ${targetUrl}${request.url} timed out after ${Date.now() - startTime}ms`);
        callback();
      }, 115000); // Slightly less than the main proxy timeout
      
      // Listen for the response to complete
      request.raw.on('close', () => {
        clearTimeout(reqTimeout);
        const duration = Date.now() - startTime;
        fastify.log.info(`Request to ${targetUrl}${request.url} completed in ${duration}ms`);
        callback();
      });
    } else {
      fastify.log.info(`Proxying ${request.method} request to ${targetUrl}${request.url}`);
    }
    
    // Ensure the raw request is directly passed to the proxy
    // This preserves the original request body for POST requests
    proxy.web(request.raw, reply.raw, options, (err) => {
      if (err) {
        fastify.log.error(`Proxy error in web(): ${err.message}`);
      }
      callback();
    });
  };
  
  // Handle completion/errors
  return new Promise<void>((resolve, reject) => {
    proxyHandler(() => {
      resolve();
    });
  });
});

// Run the server!
async function start() {
  try {
    await fastify.listen({ 
      port: 8080,
      host: '0.0.0.0' // Listen on all network interfaces
    });
  } catch (err) {
    fastify.log.error(err);
  }
}

start();

So, I am trying to setup a system that allows traffic to be redirected to a docker container based on a project ID stored in a database. I use Nginx as a reverse proxy which proxies to a fastify reverse proxy, this may not be super efficient but seems to work for GET requests but POST requests end up timing out. I am doubtful that it is the docker container it is proxying the request to that is causing the error as the logs of that docker container for a post request seem to not output anything.
Here is my code for the dynamic reverse proxy:

import Fastify, { FastifyRequest } from 'fastify';
import { db } from './db';
import { routingTable } from './db/schema';
import { eq } from 'drizzle-orm';
import dotenv from 'dotenv';
import Docker from 'dockerode';
import httpProxy from 'http-proxy';
import crypto from 'crypto';
import fastifyFormBody from '@fastify/formbody';
import fastifyMultipart from '@fastify/multipart';

dotenv.config();

const fastify = Fastify({
  logger: true,
  // Increase body size limits for proxying larger POST requests
  bodyLimit: 30 * 1024 * 1024 // 30MB
});

// Register fastify body parser plugins to handle different content types
fastify.register(fastifyFormBody);
fastify.register(fastifyMultipart, {
  limits: {
    fileSize: 25 * 1024 * 1024 // 25MB limit for file uploads
  }
});

// Create a proxy server instance
const proxy = httpProxy.createProxyServer({
  changeOrigin: true,
  xfwd: true, // Add X-Forwarded headers
  proxyTimeout: 120000, // Increase timeout to 2 minutes
  buffer: undefined, // Let the proxy handle buffering based on the request
});

// Add special handling for the proxy 
proxy.on('proxyReq', (proxyReq, req, res, options) => {
  // Ensure content-length is preserved for requests with bodies
  if (req.headers['content-length']) {
    proxyReq.setHeader('Content-Length', req.headers['content-length']);
  }
  
  // Ensure content-type is preserved 
  if (req.headers['content-type']) {
    proxyReq.setHeader('Content-Type', req.headers['content-type']);
  }
});

// Add response handling to manage incoming responses
proxy.on('proxyRes', (proxyRes, req, res) => {
  fastify.log.info(`Received proxy response from target with status: ${proxyRes.statusCode}`);
});

// Handle proxy errors
// @ts-ignore - Ignore TypeScript errors for now as the http-proxy types are hard to match exactly
proxy.on('error', function(err: any, req: any, res: any) {
  if (res.writeHead) {
    fastify.log.error(`Proxy error: ${err.message}`);
    res.writeHead(500, { 'Content-Type': 'text/plain' });
    res.end('Proxy error: ' + err.message);
  }
});
const docker = new Docker({ socketPath: '/var/run/docker.sock' });

// Define a route for handling all requests
fastify.all('*', async (request, reply) => {
  const host = request.headers.host;
  if (!host) {
    return reply.status(400).send({ error: "Host header is required" });
  }
  
  const hostParts = host.split('.');
  let subdomain = hostParts.at(0) ?? null;
  if (!subdomain) {
    return reply.status(400).send({ error: "Invalid host format" });
  }
  const useActionRunner = subdomain.includes("action");
  if (useActionRunner) {
    subdomain = subdomain.replace('-action', '');
  }
  
  // If the subdomain is "server", allow access to server endpoints directly
  if (subdomain.toLowerCase() === 'server') {
    // Skip proxying and let the request continue to other routes
    return;
  }
  
  // Query PostgreSQL for the port
  const result = await db
    .select()
    .from(routingTable)
    .where(eq(routingTable.projectId, subdomain));

  if (result.length === 0) {
    return reply.status(404).send({ error: "Project not found" });
  }

  // Use http-proxy instead of reply.from()
  const targetUrl = useActionRunner ? `http://localhost:${result[0].action_port}` : `http://localhost:${result[0].host_port}`;
  
  // Set up a custom completion handler
  const proxyHandler = (callback: () => void) => {
    // Mark the response as handled
    reply.hijack();
    
    const options: httpProxy.ServerOptions = {
      target: targetUrl,
      ignorePath: false,
      prependPath: false,
      selfHandleResponse: false,
      timeout: 120000 // Match the main proxy timeout
    };
    
    // Handle different types of requests
    if (request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH') {
      // For requests with bodies, ensure the content-type and body are preserved
      fastify.log.info(`Proxying ${request.method} request to ${targetUrl}${request.url} with Content-Type: ${request.headers['content-type']}, Content-Length: ${request.headers['content-length']}`);
      
      // Add special handling for POST requests to ensure the body gets forwarded properly
      const startTime = Date.now();
      
      // Set a longer timeout for the request to complete
      const reqTimeout = setTimeout(() => {
        fastify.log.error(`Request to ${targetUrl}${request.url} timed out after ${Date.now() - startTime}ms`);
        callback();
      }, 115000); // Slightly less than the main proxy timeout
      
      // Listen for the response to complete
      request.raw.on('close', () => {
        clearTimeout(reqTimeout);
        const duration = Date.now() - startTime;
        fastify.log.info(`Request to ${targetUrl}${request.url} completed in ${duration}ms`);
        callback();
      });
    } else {
      fastify.log.info(`Proxying ${request.method} request to ${targetUrl}${request.url}`);
    }
    
    // Ensure the raw request is directly passed to the proxy
    // This preserves the original request body for POST requests
    proxy.web(request.raw, reply.raw, options, (err) => {
      if (err) {
        fastify.log.error(`Proxy error in web(): ${err.message}`);
      }
      callback();
    });
  };
  
  // Handle completion/errors
  return new Promise<void>((resolve, reject) => {
    proxyHandler(() => {
      resolve();
    });
  });
});

// Run the server!
async function start() {
  try {
    await fastify.listen({ 
      port: 8080,
      host: '0.0.0.0' // Listen on all network interfaces
    });
  } catch (err) {
    fastify.log.error(err);
  }
}

start();
Share asked Mar 7 at 21:24 MrGreenyboyMrGreenyboy 341 silver badge5 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

When the fastify handler run, the request.body property is there, so the request.raw stream has already been read (so it is not possible to pipe it again).

You need to add a dummy content type parser:

fastify.addContentTypeParser('*', function (request, payload, done) {
  done()
})
  • Docs reference
发布评论

评论列表(0)

  1. 暂无评论