From c471627ce1a3e5f19bb14c0d0358179169df1a64 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 10 Feb 2026 20:05:38 -0800 Subject: [PATCH] fix(posthog): replace proxy rewrite with route handler for reliable body streaming (#3187) * fix(posthog): replace proxy rewrite with route handler for reliable body streaming * fix posthog --- apps/sim/app/ingest/[[...path]]/route.ts | 73 ++++++++++++++++++++++++ apps/sim/proxy.ts | 21 +------ 2 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 apps/sim/app/ingest/[[...path]]/route.ts diff --git a/apps/sim/app/ingest/[[...path]]/route.ts b/apps/sim/app/ingest/[[...path]]/route.ts new file mode 100644 index 000000000..39c537bc9 --- /dev/null +++ b/apps/sim/app/ingest/[[...path]]/route.ts @@ -0,0 +1,73 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' + +const logger = createLogger('PostHogProxy') + +const API_HOST = 'us.i.posthog.com' +const ASSET_HOST = 'us-assets.i.posthog.com' + +/** + * Builds the target PostHog URL from the incoming request path. + * Routes /ingest/static/* to the asset host, everything else to the API host. + */ +function buildTargetUrl(pathname: string, search: string): { url: string; hostname: string } { + const strippedPath = pathname.replace(/^\/ingest/, '') + const hostname = strippedPath.startsWith('/static/') ? ASSET_HOST : API_HOST + return { + url: `https://${hostname}${strippedPath}${search}`, + hostname, + } +} + +/** + * Builds forwarding headers for the PostHog request. + * Sets the Host header and strips cookies/connection headers + * that shouldn't be forwarded. + */ +function buildHeaders(request: NextRequest, hostname: string): Headers { + const headers = new Headers(request.headers) + headers.set('host', hostname) + headers.delete('cookie') + headers.delete('connection') + + return headers +} + +async function handler(request: NextRequest) { + const { url, hostname } = buildTargetUrl(request.nextUrl.pathname, request.nextUrl.search) + const headers = buildHeaders(request, hostname) + + const hasBody = !['GET', 'HEAD'].includes(request.method) + + try { + const response = await fetch(url, { + method: request.method, + headers, + ...(hasBody ? { body: request.body, duplex: 'half' } : {}), + } as RequestInit) + + const responseHeaders = new Headers(response.headers) + responseHeaders.delete('content-encoding') + responseHeaders.delete('content-length') + responseHeaders.delete('transfer-encoding') + + return new NextResponse(response.body, { + status: response.status, + headers: responseHeaders, + }) + } catch (error) { + logger.error('PostHog proxy error', { + url, + method: request.method, + error: error instanceof Error ? error.message : String(error), + }) + return new NextResponse(null, { status: 502 }) + } +} + +export const GET = handler +export const POST = handler +export const PUT = handler +export const PATCH = handler +export const DELETE = handler +export const OPTIONS = handler diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index c90df2eec..36ada3484 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -140,24 +140,6 @@ function handleSecurityFiltering(request: NextRequest): NextResponse | null { export async function proxy(request: NextRequest) { const url = request.nextUrl - if (url.pathname.startsWith('/ingest/')) { - const hostname = url.pathname.startsWith('/ingest/static/') - ? 'us-assets.i.posthog.com' - : 'us.i.posthog.com' - - const targetPath = url.pathname.replace(/^\/ingest/, '') - const targetUrl = `https://${hostname}${targetPath}${url.search}` - - return NextResponse.rewrite(new URL(targetUrl), { - request: { - headers: new Headers({ - ...Object.fromEntries(request.headers), - host: hostname, - }), - }, - }) - } - const sessionCookie = getSessionCookie(request) const hasActiveSession = isAuthDisabled || !!sessionCookie @@ -219,7 +201,6 @@ export async function proxy(request: NextRequest) { export const config = { matcher: [ - '/ingest/:path*', // PostHog proxy for session recording '/', // Root path for self-hosted redirect logic '/terms', // Whitelabel terms redirect '/privacy', // Whitelabel privacy redirect @@ -230,6 +211,6 @@ export const config = { '/signup', '/invite/:path*', // Match invitation routes // Catch-all for other pages, excluding static assets and public directories - '/((?!_next/static|_next/image|favicon.ico|logo/|static/|footer/|social/|enterprise/|favicon/|twitter/|robots.txt|sitemap.xml).*)', + '/((?!_next/static|_next/image|ingest|favicon.ico|logo/|static/|footer/|social/|enterprise/|favicon/|twitter/|robots.txt|sitemap.xml).*)', ], }