If you've ever used ngrok for local development, you know how good the built-in request inspector is. You open localhost:4040, and there's a live dashboard showing every request hitting your tunnel — full headers, request and response bodies, status codes, timing. It's one of those tools that becomes indispensable the moment you use it.
Then you switch to Cloudflare Tunnels.
cloudflared is fantastic — it's free, it's fast, and it integrates beautifully with Cloudflare's ecosystem. But it has one glaring gap: there's no request inspector. You're flying blind. A webhook fires and you have no idea what payload it sent. A request fails and you can't see the headers. You're left sprinkling console.log statements everywhere and hoping for the best.
So I built one.
Introducing Tunnel Inspector
Tunnel Inspector is a lightweight, open source tool that gives cloudflared the inspector it always should have had.
It works by placing a small proxy between cloudflared and your app. The proxy captures every request and response, then streams them in real time to a polished local dashboard built with Next.js.
Internet → cloudflared → Proxy (:8080) → Your App (:3000)
↓
SSE (:4040)
↓
Inspector UI (/inspector)
All traffic is inspected locally and never leaves your machine.
The Problem in Detail
Here's a typical debugging session before Tunnel Inspector:
- You're building a Stripe webhook handler
- Stripe fires an event to your
cloudflaredtunnel - Something goes wrong — your handler throws an error
- You have no idea if the payload arrived malformed, if a header was missing, or if the signature verification failed
- You add logging, redeploy, trigger the event again, check your terminal, repeat
With Tunnel Inspector, step 4 becomes: open the dashboard, click the request, see exactly what Stripe sent.
How It Works
The proxy server
The core of the tool is a single file — server/proxy.mjs — built with zero npm dependencies. Just Node.js's built-in http module.
Every incoming request goes through this flow:
1. Request arrives at :8080
2. Body is buffered (chunks collected as they stream in)
3. Request forwarded to your app at :3000
4. Response captured (status, headers, body)
5. Entry stored in circular buffer (max 200)
6. Entry broadcast to all connected SSE clients
7. UI updates instantly
Here's the core of the capture logic:
const proxyServer = http.createServer((req, res) => {
const id = generateId();
const startedAt = new Date().toISOString();
const start = performance.now();
const reqChunks = [];
req.on("data", (chunk) => reqChunks.push(chunk));
req.on("end", () => {
const reqBody = tryParseJson(Buffer.concat(reqChunks).toString("utf-8"));
const proxyReq = http.request({ ...options }, (proxyRes) => {
const resChunks = [];
proxyRes.on("data", (chunk) => resChunks.push(chunk));
proxyRes.on("end", () => {
const entry = {
id, method: req.method, url: req.url,
status: proxyRes.statusCode,
duration: Math.round(performance.now() - start),
startedAt, reqHeaders: req.headers, reqBody,
resHeaders: proxyRes.headers,
resBody: tryParseJson(Buffer.concat(resChunks).toString("utf-8")),
};
buffer.unshift(entry);
broadcast("response", entry); // SSE to all clients
// forward response back to original caller
});
});
});
});
The key insight is that we're collecting the full body before forwarding, which means we can inspect it — but we're also forwarding it faithfully so your app receives it exactly as it arrived.
The SSE server
The second server runs on :4040 and handles two things:
-
GET /events— an SSE stream. New clients immediately receive the full history as ahistoryevent, then getresponseevents in real time as new requests come in -
GET /api/requests— returns the current buffer as JSON, useful for tooling
if (req.url === "/events") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
// Send full history on connect
res.write(`event: history\ndata: ${JSON.stringify(buffer)}\n\n`);
sseClients.add(res);
req.on("close", () => sseClients.delete(res));
}
The React hook
On the frontend, a custom hook manages the SSE connection:
export function useInspectorFeed() {
const [requests, setRequests] = useState<InspectorRequest[]>([]);
const [connected, setConnected] = useState(false);
useEffect(() => {
const es = new EventSource("http://localhost:4040/events");
es.addEventListener("history", (e) => {
setRequests(JSON.parse(e.data));
});
es.addEventListener("response", (e) => {
const entry = JSON.parse(e.data);
setRequests((prev) => [entry, ...prev].slice(0, 200));
});
es.onerror = () => {
setConnected(false);
es.close();
setTimeout(connect, 3000); // auto-reconnect
};
}, []);
return { requests, connected, clear };
}
No polling, no WebSocket complexity, no library. Server-Sent Events are a perfect fit here — the data only ever flows one way (server to browser), and they reconnect automatically.
The Inspector UI
The dashboard is a split-panel layout built with Next.js, Tailwind CSS, and React 19.
Left panel — request list
Each row shows a colour-coded method badge (GET in teal, POST in amber, DELETE in red, PUT in blue, PATCH in purple), the URL path, status code badge, and response time. New requests slide in at the top with a subtle fade animation.
The toolbar has a text search (filters by method + URL) and method filter chips.
Right panel — detail view
Three tabs per request:
Overview — metadata grid (method, status, duration, timestamp, full URL) plus a side-by-side view of request and response headers.
Request — full request headers and body.
Response — full response headers and body.
Body viewer
The body viewer is one of the more interesting parts of the project. It auto-detects the content format and applies syntax highlighting:
| Format | What it highlights |
|---|---|
| JSON | Keys in indigo, strings in emerald, numbers in amber, booleans/null in purple |
| GraphQL-over-JSON | Splits the JSON into Operation, Query (GraphQL syntax highlighted), and Variables |
| HTML / XML | Tags, attributes, attribute values, comments |
| JavaScript | Keywords, strings, types, identifiers, comments |
| Form data | Decoded key-value pairs in a structured table |
| Multipart | Parsed parts with name, filename, content-type |
| Plain text | Monospaced fallback |
Every code block has a copy-to-clipboard button.
The GraphQL-over-JSON detection was particularly satisfying to build — it checks if the parsed JSON has a query field that starts with a GraphQL keyword, then splits the display into separate sections rather than treating it as a generic JSON blob.
Getting Started
1. Clone and install
git clone https://github.com/BlockbusterAndy/cloudflare-tunnel-inspector.git
cd cloudflare-tunnel-inspector
npm install
2. Update your cloudflared config
# ~/.cloudflared/config.yml
ingress:
- hostname: yourdomain.com
service: http://localhost:8080 # point to proxy, not your app
- service: http_status:404
3. Start everything
npm run dev:inspect
This runs both the Next.js dev server and the proxy concurrently. Open http://localhost:3000/inspector and send some traffic through your tunnel.
Design system
The UI uses a custom design system called Obsidian Lens — a deep dark aesthetic where hierarchy is expressed through tonal shifts rather than borders.
The surface hierarchy goes:
#131315 → #201f22 → #2a2a2c → #353437
base container high highest
No solid borders between sections. Just carefully chosen background shades that create a sense of depth. Combined with JetBrains Mono for all technical data and Inter for UI labels, it gives the inspector a premium developer tool feel rather than a generic dark theme.
What's next
The project is open source and actively looking for contributors. Areas I'd love help with:
- WebSocket inspection — currently WS upgrade requests are forwarded but not captured
- Request replay — re-fire any captured request directly from the UI
-
CLI wrapper —
npx tunnel-inspectorthat wraps any dev server -
Export — download the captured buffer as
.ndjson - Status code filtering — filter the list by 2xx / 4xx / 5xx
If any of these interest you, the repo has good first issue labels and a full CONTRIBUTING.md to help you get started.
Wrapping up
The core insight behind this project is simple: the proxy pattern is extremely powerful for observability. By placing a thin layer between two services, you get full visibility into what's flowing between them with zero changes to either side.
The whole proxy is 120 lines of vanilla Node.js. No framework, no dependencies. The rest is just a nice UI on top.
If you use cloudflared in your development workflow, give it a try. And if you've built something similar or have ideas for making it better, I'd love to hear from you.
GitHub: https://github.com/BlockbusterAndy/cloudflare-tunnel-inspector
Built with Next.js 16, React 19, Tailwind CSS 4, and zero-dependency Node.js.
Top comments (0)