umanggarg Claude Sonnet 4.6 commited on
Commit
4e2f438
·
1 Parent(s): 75b428c

Fix PostHog proxy: use Node.js stream piping instead of fetch

Browse files

Previous fetch-based approach had body re-encoding issues causing 405.
Stream piping forwards request bytes verbatim — preserving PostHog's
gzip/base64 compression and all headers without transformation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. ui/api/ph/[...path].js +27 -41
  2. ui/src/main.jsx +2 -3
ui/api/ph/[...path].js CHANGED
@@ -1,56 +1,42 @@
1
  /**
2
  * Vercel Serverless Function — PostHog reverse proxy.
3
  *
4
- * Why: ad blockers match on "us.i.posthog.com" and block analytics with
5
- * ERR_BLOCKED_BY_CLIENT. Routing through our own domain (/api/ph/*) makes
6
- * requests look first-party, bypassing the block.
7
  *
8
- * Using Node.js runtime (not Edge) because Edge runtime requires Next.js
9
- * for Vercel's file-based routing to work correctly. Node.js runtime
10
- * supports all HTTP methods and body types natively.
11
  */
12
- const POSTHOG_HOST = 'https://us.i.posthog.com';
13
 
14
- export default async function handler(req, res) {
15
- // Reconstruct the PostHog path from the wildcard [...path] segment
16
- const segments = req.query.path || [];
17
- const pathStr = Array.isArray(segments) ? segments.join('/') : segments;
18
- const query = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
19
- const targetUrl = `${POSTHOG_HOST}/${pathStr}${query}`;
20
 
21
- // Read the incoming body as a raw buffer (preserves gzip/base64 compression
22
- // that PostHog JS applies — do NOT parse or re-encode it)
23
- const bodyChunks = [];
24
- for await (const chunk of req) {
25
- bodyChunks.push(chunk);
26
- }
27
- const rawBody = bodyChunks.length > 0 ? Buffer.concat(bodyChunks) : undefined;
28
 
29
- // Forward only the headers PostHog needs; strip Vercel-specific ones
30
- const forwardedHeaders = {
31
- host: 'us.i.posthog.com',
32
- 'content-type': req.headers['content-type'] || 'text/plain',
33
- 'user-agent': req.headers['user-agent'] || 'posthog-proxy',
 
 
 
34
  };
35
- if (req.headers['content-encoding']) {
36
- forwardedHeaders['content-encoding'] = req.headers['content-encoding'];
37
- }
38
 
39
- const proxyResp = await fetch(targetUrl, {
40
- method: req.method,
41
- headers: forwardedHeaders,
42
- body: ['GET', 'HEAD'].includes(req.method) ? undefined : rawBody,
43
  });
44
 
45
- // Forward response status + headers back to the browser
46
- res.status(proxyResp.status);
47
- proxyResp.headers.forEach((value, key) => {
48
- // Skip headers that Node's http module manages itself
49
- if (!['transfer-encoding', 'connection'].includes(key)) {
50
- res.setHeader(key, value);
51
- }
52
  });
53
 
54
- const responseBody = await proxyResp.arrayBuffer();
55
- res.send(Buffer.from(responseBody));
56
  }
 
1
  /**
2
  * Vercel Serverless Function — PostHog reverse proxy.
3
  *
4
+ * Ad blockers match on "us.i.posthog.com" and drop requests. Routing through
5
+ * our own Vercel domain (/api/ph/*) makes them look first-party.
 
6
  *
7
+ * Uses Node.js stream piping (req.pipe proxyRes.pipe res) so the body
8
+ * is forwarded byte-for-byte with no parsing preserving PostHog's gzip/base64
9
+ * compression without any re-encoding issues.
10
  */
11
+ import https from 'https';
12
 
13
+ export default function handler(req, res) {
14
+ const segments = Array.isArray(req.query.path)
15
+ ? req.query.path.join('/')
16
+ : (req.query.path || '');
 
 
17
 
18
+ const query = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
19
+ const path = `/${segments}${query}`;
 
 
 
 
 
20
 
21
+ const options = {
22
+ hostname: 'us.i.posthog.com',
23
+ path,
24
+ method: req.method,
25
+ headers: {
26
+ ...req.headers,
27
+ host: 'us.i.posthog.com',
28
+ },
29
  };
 
 
 
30
 
31
+ const proxyReq = https.request(options, (proxyRes) => {
32
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
33
+ proxyRes.pipe(res);
 
34
  });
35
 
36
+ proxyReq.on('error', (err) => {
37
+ res.status(500).json({ error: err.message });
 
 
 
 
 
38
  });
39
 
40
+ // Pipe the incoming body directly — no buffering, no re-encoding
41
+ req.pipe(proxyReq);
42
  }
ui/src/main.jsx CHANGED
@@ -6,9 +6,8 @@ import './index.css'
6
  import App from './App.jsx'
7
 
8
  posthog.init('phc_B4VarKaWfNc3u7vMcsUPRDbNgSyVxaBqtYT3ZwP6FshM', {
9
- // Route through Vercel proxy (/ph/*) so ad blockers don't block requests
10
- // to us.i.posthog.com directly. The proxy is configured in vercel.json.
11
- // Route through Vercel Edge Function (/api/ph/*) — bypasses ad blockers
12
  api_host: '/api/ph',
13
  ui_host: 'https://us.posthog.com',
14
  capture_pageview: true,
 
6
  import App from './App.jsx'
7
 
8
  posthog.init('phc_B4VarKaWfNc3u7vMcsUPRDbNgSyVxaBqtYT3ZwP6FshM', {
9
+ // Route through /api/ph (Vercel serverless proxy) so ad blockers
10
+ // don't block requests going directly to us.i.posthog.com.
 
11
  api_host: '/api/ph',
12
  ui_host: 'https://us.posthog.com',
13
  capture_pageview: true,