Skip to content

Instantly share code, notes, and snippets.

@arpitdalal
Last active October 30, 2025 21:55
Show Gist options
  • Select an option

  • Save arpitdalal/ccc807fa6a15638b86a128d9b7ac51a1 to your computer and use it in GitHub Desktop.

Select an option

Save arpitdalal/ccc807fa6a15638b86a128d9b7ac51a1 to your computer and use it in GitHub Desktop.
A reverse proxy for PostHog using react-router
import { type LoaderFunctionArgs, type ActionFunctionArgs } from "react-router";
const API_HOST = "us.i.posthog.com"; // use eu.i.posthog.com for EU
const ASSET_HOST = "us-assets.i.posthog.com"; // use eu-assets.i.posthog.com for EU
type RequestInitWithDuplex = RequestInit & {
duplex?: "half"; // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1483
};
async function posthogProxy(request: Request) {
const url = new URL(request.url);
// Choose the right host based on the request path
const hostname = url.pathname.startsWith("/resources/ingest/static")
? ASSET_HOST
: API_HOST;
// Build the new URL to forward to PostHog
const newUrl = new URL(url);
newUrl.protocol = "https";
newUrl.hostname = hostname;
newUrl.port = "443";
newUrl.pathname = newUrl.pathname.replace(/^\/resources\/ingest/, "");
// ^ depends on the endpoint
// Prepare headers
const headers = new Headers(request.headers);
headers.set("host", hostname);
headers.delete("accept-encoding"); // to let the fetch handle compression
// Setup fetch options
const fetchOptions: RequestInitWithDuplex = {
method: request.method,
headers,
redirect: "follow", // enable fetch to follow the redirect in case PostHog throws a redirect
};
// Add body for non-GET/HEAD requests
if (!["GET", "HEAD"].includes(request.method)) {
fetchOptions.body = request.body;
fetchOptions.duplex = "half"; // needed for larger POST bodies
}
try {
// Forward the request to PostHog
const response = await fetch(newUrl, fetchOptions);
// Clean up response headers
const responseHeaders = new Headers(response.headers);
responseHeaders.delete("content-encoding");
responseHeaders.delete("content-length");
responseHeaders.delete("transfer-encoding");
responseHeaders.delete("Content-Length");
// Get response data if needed
const data =
response.status === 304 || response.status === 204
? null
: await response.arrayBuffer();
// Return the response
return new Response(data, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (error) {
// Track and log the error
console.error("Proxy error:", error);
return new Response("Proxy Error", { status: 500 });
}
}
export async function loader({ request }: LoaderFunctionArgs) {
return await posthogProxy(request);
}
export async function action({ request }: ActionFunctionArgs) {
return await posthogProxy(request);
}
export posthog.init('<YOUR_API_KEY>', {
api_host: '/resources/ingest' // point to the proxy endpoint instead of PostHog's servers
ui_host: 'https://us.posthog.com', // use https://eu.posthog.com for EU
// ^ Necessary when using reverse proxy - https://posthog.com/docs/libraries/js#config
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment