fix(posthog): replace proxy rewrite with route handler for reliable body streaming

This commit is contained in:
Waleed Latif
2026-02-10 19:32:37 -08:00
parent 78fef22d0e
commit edfddebc03
2 changed files with 79 additions and 20 deletions

View File

@@ -0,0 +1,78 @@
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, forwards client IP for geolocation,
* 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)
const forwardedFor = request.headers.get('x-forwarded-for')
if (forwardedFor) {
headers.set('x-forwarded-for', forwardedFor)
}
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('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

View File

@@ -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).*)',
],
}