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
1 Answer
Reset to default 0When 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