{"id":51002109,"url":"https://github.com/yeet-src/httpinspect","last_synced_at":"2026-06-21T16:01:08.167Z","repository":{"id":365560505,"uuid":"1272668355","full_name":"yeet-src/httpinspect","owner":"yeet-src","description":"top, but for the HTTP endpoints on your host — a live dashboard of the most active plaintext HTTP endpoints, right in the terminal.","archived":false,"fork":false,"pushed_at":"2026-06-19T23:55:24.000Z","size":604,"stargazers_count":138,"open_issues_count":0,"forks_count":10,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-20T15:38:38.289Z","etag":null,"topics":["ebpf","http","interactive","javascript","observability","showcase","tui"],"latest_commit_sha":null,"homepage":"https://yeet.cx","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yeet-src.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-17T20:40:15.000Z","updated_at":"2026-06-20T15:34:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yeet-src/httpinspect","commit_stats":null,"previous_names":["yeet-src/httpinspect"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/yeet-src/httpinspect","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fhttpinspect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fhttpinspect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fhttpinspect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fhttpinspect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yeet-src","download_url":"https://codeload.github.com/yeet-src/httpinspect/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fhttpinspect/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34616512,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ebpf","http","interactive","javascript","observability","showcase","tui"],"created_at":"2026-06-20T15:33:12.540Z","updated_at":"2026-06-21T16:01:08.046Z","avatar_url":"https://github.com/yeet-src.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `httpinspect`\n\n\u003e **`top` for the HTTP endpoints on your host.** Every plaintext HTTP request crossing the box — decoded off the wire and ranked live in your terminal by traffic, rate, and latency. No proxy, no sidecar, no app changes.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/platform-Linux-1793D1\" alt=\"Linux\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/built%20with-yeet%20%2B%20eBPF-8A2BE2\" alt=\"yeet + eBPF\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-GPL--2.0-3DA639\" alt=\"GPL-2.0\"\u003e\n  \u003ca href=\"https://discord.gg/dYZu9PjKB\"\u003e\u003cimg src=\"https://img.shields.io/badge/chat-Discord-5865F2\" alt=\"Discord\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/http-endpoint.gif\" alt=\"httpinspect — a live HTTP endpoint dashboard in the terminal\" width=\"820\"\u003e\n\u003c/p\u003e\n\n**`httpinspect` turns the HTTP requests crossing your host into a live `top`-style table** — every endpoint sorted by traffic, with a running request count, a per-second rate, and how long ago each was last hit. Open one and you get a focused detail screen: on-the-wire latency (p50 / p95 / max), status-code mix, and req/s and latency sparklines — all built on eBPF, all reading the bytes straight off the wire.\n\n\u003e [!TIP]\n\u003e **No proxy, no port to point at, no app changes.** `httpinspect` attaches eBPF programs at the TC layer and reads HTTP request lines straight off the wire as packets flow through the kernel — including loopback, so requests between local services are covered too. Your traffic is never intercepted, held, or delayed.\n\n## Quick start\n\n```sh\ncurl -fsSL https://yeet.cx | sh\nmake            # compile bin/httptop.bpf.o (needs clang + bpftool)\nyeet run .      # watch every up interface, including loopback\n```\n[Manual install guide](https://yeet.cx/docs/install/manual-installation) | Linux only\n\nWith any plaintext HTTP flowing on the box, that's it — `httpinspect` enumerates the up interfaces, attaches at the TC layer, and starts ranking endpoints. Flags tune what it watches and how it groups:\n\n| flag             | default       | meaning                                                              |\n| ---------------- | ------------- | -------------------------------------------------------------------- |\n| `--iface=\u003clist\u003e` | all up ifaces | comma-separated interface names to watch, e.g. `--iface=lo,eth0`     |\n| `--keep-query`   | off           | keep query strings distinct — `/x?id=1` and `/x?id=2` stay separate rows instead of collapsing into one |\n\n```sh\nyeet run . --iface lo,eth0   # only these interfaces\nyeet run . --keep-query      # /x?id=1 and /x?id=2 stay separate rows\n```\n\nRuns until `Ctrl-C`. Resize the terminal and the table reflows; needs a real terminal (it's a TUI — don't pipe or redirect the output).\n\n## A 30-second primer on HTTP-on-the-wire\n\nThe mental model for what `httpinspect` reads:\n\n**A request is text.** An HTTP/1.x request starts with a request line — `GET /path HTTP/1.1` — followed by headers, one per line, then a blank line. The very first bytes of the TCP payload *are* that line.\n\n**The endpoint is `METHOD host path`.** The method and path come from the request line; the host comes from the `Host:` header (or the absolute-form target on a proxied/`CONNECT` request). `httpinspect` tallies traffic by that triple.\n\n**Plaintext only.** This works because the bytes on the wire *are* the request. Under TLS (`https://`) the payload is ciphertext at this layer, so HTTPS is invisible — see the caveats.\n\n## Common use cases\n\n`httpinspect` is for anyone who wants a ground-truth picture of the plaintext HTTP actually crossing a host — not what an app's own access log claims it served.\n\n- A service is slow. Which endpoint is getting hammered, and at what rate?\n- You suspect a retry storm. Watch a path's `REQ/S` spike in real time.\n- Auditing a box: what plaintext HTTP is actually flowing, and to which hosts?\n- Local microservices talking over `lo` — see the chatter without instrumenting any of them.\n\n## What you're looking at\n\n```\nhttpinspect · watching all (3) · plaintext HTTP only\n #  METHOD  HOST            PATH              COUNT   REQ/S   LAST\n 1  GET     shop.internal   /api/products      1843    27     0s\n 2  POST    auth.internal   /login              512     4      1s\n 3  GET     shop.internal   /health             318     ·      3s\n```\n\nA **status bar** names the app, the interfaces being watched, and a reminder that this is plaintext HTTP only. The **table** is one row per `METHOD host path` endpoint, sorted by total count (busiest first), capped to what fits the terminal. The **footer** carries total requests, distinct endpoints, total bytes seen on the wire, and uptime.\n\n| column   | meaning                                                          |\n| -------- | --------------------------------------------------------------- |\n| `#`      | rank by total count                                             |\n| `METHOD` | HTTP method, color-coded (GET, POST, PUT, …)                    |\n| `HOST`   | `Host:` header (or authority from an absolute-form target)      |\n| `PATH`   | request path; query string collapsed unless `--keep-query`      |\n| `COUNT`  | cumulative requests seen for this endpoint                      |\n| `REQ/S`  | requests in the last second (`·` when idle)                     |\n| `LAST`   | how long ago this endpoint was last hit                         |\n\nColors come from yeet's terminal styling and no-op to plain text when stdout isn't a TTY — but `httpinspect` is a TUI and needs a real terminal, so it refuses to run piped or redirected rather than render garbage.\n\n## Navigation\n\nThe dashboard is interactive:\n\n| key                        | action                                              |\n| -------------------------- | --------------------------------------------------- |\n| `↑` / `↓` (or `k` / `j`)   | move the selection up/down the endpoint list        |\n| `PgUp` / `PgDn`            | jump ten rows                                       |\n| `Enter`                    | open the **detail screen** for the highlighted endpoint |\n| `Esc` (or `←`)             | return to the list                                  |\n| `q`                        | back to the list (in detail) / quit (in the list)   |\n| `Ctrl-C`                   | quit                                                |\n\nThe **detail screen** is a focused, live breakdown of one `METHOD host path` endpoint:\n\n- total requests and its share of all traffic, current and peak req/s\n- **latency** (p50 / p95 / max) — derived by pairing each response with its request on the wire (see below)\n- **status codes** seen, color-coded by class (2xx/3xx/4xx/5xx)\n- bytes on the wire, first/last-seen ages\n- block sparklines of req/s and of recent response latency\n\nIt updates in place as new traffic arrives — no need to back out and re-enter.\n\n## How it works\n\nThe core is in [`httptop.bpf.c`](httptop.bpf.c) and the JS rendering layer rooted at [`main.jsx`](main.jsx).\n\n### The BPF side\n\nA single BPF object attaches two TC (`tcx`) programs, auto-attached on `start()` by their `SEC()` names, and ships decoded events to userspace over a ring buffer:\n\n| program      | hook           | what it captures                                                       |\n| ------------ | -------------- | ---------------------------------------------------------------------- |\n| `on_ingress` | `tcx/ingress`  | inbound TCP segments — requests arriving / responses returning         |\n| `on_egress`  | `tcx/egress`   | outbound TCP segments — requests this host sends / responses it serves |\n\nFor each segment the program does a cheap in-kernel check on the first payload bytes: does it begin with an HTTP method token (`GET `, `POST `, …) — a **request** — or with `HTTP/` — a **response** status line? Only those two cross the ring buffer (responses capped short, since only the status line is needed); ACKs and non-HTTP traffic are dropped in the kernel. Every event carries a monotonic kernel timestamp.\n\nThe one map connecting kernel to userspace is `events` — a `RINGBUF` bound by its `btf_struct` (`http_event`), one decoded record per captured segment.\n\n### The JS side\n\nThe dashboard runs in yeet's V8 runtime, subscribing to that ring buffer and rendering the terminal UI with `yeet:tui`:\n\n| file            | responsibility                                                                                  |\n| --------------- | ----------------------------------------------------------------------------------------------- |\n| `main.jsx`      | entry: tty guard, interface discovery, BPF attach, ringbuf subscribe, mount, key input          |\n| `state.js`      | live data + request/response ingest, latency pairing, status tally, rate ticks, selection       |\n| `render.jsx`    | formatters, method colors, column widths, sparkline (pure)                                      |\n| `dashboard.jsx` | panels + layout: the list and endpoint-detail screens (the Dashboard view)                      |\n\nIn userspace, each response is paired with the oldest unmatched request on the same flow — the unordered port pair, since a response travels the reverse direction. The timestamp delta is the **on-the-wire latency**, and the status line gives the **code**; both are aggregated per endpoint.\n\n### Why TC, not a proxy or a syscall wrapper\n\nReading requests at the TC layer means there's nothing to point traffic through and no app to reconfigure — the programs observe and copy request segments as the kernel moves them, including loopback, so local service-to-service chatter is covered without instrumenting anything. And because the method/`HTTP/` check happens in the kernel, ACKs and non-HTTP traffic never cost a ring-buffer write.\n\n## Requirements\n\n\u003e [!IMPORTANT]\n\u003e Linux with **BTF** (`CONFIG_DEBUG_INFO_BTF=y`) — needed for the TC context structs and the `sock` types the programs read. Default on current Arch, Fedora, Ubuntu, and Debian 12+. CO-RE means no per-kernel recompile.\n\u003e\n\u003e A reasonably recent kernel with **TCX** support (`tcx` links, Linux 6.6+), plus the yeet daemon, which handles the privileged BPF load. `curl -fsSL https://yeet.cx | sh` installs it.\n\n## Honest caveats\n\n\u003e [!NOTE]\n\u003e `httpinspect` is observability, not enforcement. It tells you what crossed the wire; it does not stop, hold, or modify anything.\n\n- **Plaintext HTTP only.** TLS payloads are ciphertext at this layer, so HTTPS is invisible. Capturing it would need a uprobe on `SSL_write`/`SSL_read`, which is a different tool. ([contact us](https://yeet.cx/?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=httpinspect\u0026utm_content=caveats-tls) for custom yeet scripts)\n- Only the captured prefix (512 bytes) of each request is parsed — enough for the request line and `Host` header, which is all the table needs.\n- **Latency is on-the-wire, not server-internal.** It's the time between the request and response segments as seen at this host's TC layer, so it includes network RTT for remote hosts. Responses are paired to requests FIFO per flow, which is correct for ordered HTTP/1.x but approximate under pipelining; unmatched requests are dropped after 10s.\n- Loopback packets are seen twice (egress and ingress on `lo`); identical 4-tuple+seq sightings are de-duplicated so they're not double-counted.\n- Under heavy load or a slow link, some segments may not be captured, so counts are a close lower bound rather than an exact tally.\n- IPv6 packets carrying TCP behind extension-header chains (rare) are skipped.\n\n## Community questions\n\n**Does this need a proxy or a sidecar?**\nNo. `httpinspect` reads requests off the wire from inside the kernel's TC layer, so there's nothing to point traffic through and no app to reconfigure.\n\n**Will it slow down or intercept my traffic?**\nNo. The programs observe and copy request segments; they don't hold, modify, or redirect packets.\n\n**Why don't I see my HTTPS traffic?**\nBecause it's encrypted before it hits the wire. At the TC layer the payload is ciphertext, so there's no request line to parse. That's a fundamental limit of capturing here, not a bug.\n\n**Why is a local service showing as `127.0.0.1:port`?**\nThat's the `Host:` header the client sent. Services addressed by name show their name; those addressed by IP show the IP.\n\n**Can I get a quick check without the full TUI?**\nYes. `yeet run verify.js` attaches the probe, aggregates for ~4s, and prints the counts before exiting — a headless sanity check of the capture + parse pipeline.\n\n## Building from source\n\n```sh\nmake          # generates include/vmlinux.h, builds bin/httptop.bpf.o\nmake vmlinux  # force-refresh the kernel type header\nmake clean    # remove the build artifacts\n```\n\nNeeds `clang` (BPF target) and `bpftool`, plus your distro's `libbpf` / `libbpf-dev` for headers. The generated `include/vmlinux.h` and `bin/` are build artifacts (gitignored).\n\n## License\n\nGPL-2.0. The BPF program declares `char LICENSE[] SEC(\"license\") = \"GPL\"` in [`httptop.bpf.c`](httptop.bpf.c), required for the kernel helpers it uses.\n\n---\n\nBuilt with [yeet](https://yeet.cx/docs/?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=httpinspect), a JS runtime for writing eBPF programs and live system dashboards on Linux. Join us on [discord](https://discord.gg/dYZu9PjKB?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=httpinspect).\n\u003c/content\u003e\n\u003c/invoke\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fhttpinspect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyeet-src%2Fhttpinspect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fhttpinspect/lists"}