posts

Mar 22, 2026

Debugging HTTP traffic with tcpdump and tshark

Estimated Reading Time: 12 minutes (2371 words)

There are times when you need to debug an issue in your HTTP service and realize that your logs don’t have enough information. One way to troubleshoot is to capture network traffic and peek into request and response data.

Sounds hard? It isn’t. You can do that with tcpdump, then analyze it with Wireshark or its CLI tool, tshark.

Here’s a quick write-up on how to capture your HTTP traffic with tcpdump and filter it with tshark. Jump to the cheat sheet here if you’re too lazy to read the whole thing or follow along.

In this tutorial, we’re focusing on capturing and analyzing HTTP traffic. If your services use HTTPS, the packets are encrypted and won’t be readable without credentials.

Dealing with HTTPS is an article for another day.

Prerequisites

If you want to follow along, here are the tools we need:

Here’s a minimal Python server implementation with a JSON endpoint, which we will use later when extracting JSON responses:

from http.server import HTTPServer, BaseHTTPRequestHandler
import json

BOOKS = {
    "books": [
        {"name": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
        {"name": "1984", "author": "George Orwell"},
    ]
}

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/books":
            body = json.dumps(BOOKS).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404)
            self.end_headers()

if __name__ == "__main__":
    print("Server running at http://localhost:8080/books")
    HTTPServer(("localhost", 8080), Handler).serve_forever()

Save this as server.py, then run it with python3 server.py. Use curl localhost:8080/books to simulate traffic with a JSON response.

Capturing traffic

Disclaimer

The captured traffic may contain sensitive data such as credentials, cookies, and personal information. If you are doing this in a production environment, please make sure you have gotten approval from whoever is in charge.

Capturing traffic with tcpdump1 is straightforward:

Capture traffic with tcpdump

# Capturing traffic on port 8080 on any network interface (-i any)
sudo tcpdump -i any port 8080

If you have a server running at port 8080 already, just run curl localhost:8080 to see some output.

Most of the time, you’ll want to capture traffic and write it to a file. You can do that by appending -w <filename>.pcap to the previous command:

Write capture to a PCAP file

sudo tcpdump -i any port 8080 -w output.pcap

If you are following along, run the following curl commands to generate some GET and POST requests with JSON payloads.

Generate some HTTP traffic with curl

curl localhost:8080
curl localhost:8080/books
curl localhost:8080/books
curl -X POST http://localhost:8080/books \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Sample Book",
    "author": "John Doe"
  }'

You’ll notice that no console output is shown while capturing. Use Ctrl+C to stop.

tcpdump: data link type PKTAP
tcpdump: listening on any, link-type PKTAP (Apple DLT_PKTAP), snapshot length 524288 bytes
^C252 packets captured
8469 packets received by filter
0 packets dropped by kernel

You’ll see a summary of how many packets were captured and received when it stops.

The output is in PCAP (Packet Capture) file format. We’ll need some tools to parse and read its contents. That’s where tshark comes in.

Filtering and formatting the captured traffic

First, install tshark following the instructions here. Then we can use tshark to inspect the captured traffic:

Read captured traffic with tshark

# -r to specify file to read
tshark -r output.pcap

You can filter by protocol using -Y. For example, to show only captured HTTP traffic:

Filter only HTTP packets

# -Y stands for display filter.
tshark -r output.pcap -Y 'http'

Not very readable, right? No worries, tshark supports different output formats such as json and fields. You can configure that through -T:

Use --help to show supported output format

tshark --help
-T pdml|ps|psml|json|jsonraw|ek|tabs|text|fields|?
-j <protocolfilter>      protocols layers filter if -T ek|pdml|json selected
-J <protocolfilter>      top level protocol filter if -T ek|pdml|json selected
-e <field>               field to print if -Tfields selected (e.g. tcp.port,
-E<fieldsoption>=<value> set options for output when -Tfields selected:
--no-duplicate-keys      If -T json is specified, merge duplicate keys in an object

We can use -T fields in combination with -e to control which fields are printed.

Show selected HTTP fields

# -T is to configure the output.
# -e is to select the fields to print
tshark -r output.pcap -Y 'http' -T fields -e tcp.stream -e frame.time -e http.request.method -e http.request.uri -e http.response.code

You can use tshark -G fields to see all available fields. It prints every available field, so you’ll want to filter it further using rg or grep:

Discover available tshark fields

tshark -G fields | rg "http\."

The -Y argument can also be used to filter output further. For example, we can use the following filter to show all traffic that has a 200 status code:

Filter by HTTP status code

tshark -r output.pcap -Y 'http.response.code == 200' -T fields -e tcp.stream -e frame.time -e http.request.method -e http.request.uri -e http.response.code

Or filter by a specific tcp.stream:

Filter by TCP stream

tshark -r output.pcap -Y 'tcp.stream == 9 and http'

When dealing with JSON requests and responses, we can configure tshark to output packet data as JSON (-T json), and use jq to extract the fields you care about.

For example, to extract the JSON response of every successful HTTP response:

Extract JSON payload values with jq

tshark -r output.pcap -Y 'http.response.code == 200' -T json 2>/dev/null | jq -r '.[]._source.layers.json."json.object"'

We can also use a similar approach to extract the request payload in JSON; it’s just a bit more complicated:

Extract JSON payload values with jq

tshark -r output.pcap -Y 'http.request' -T json 2>/dev/null \
| jq '.[] | ._source.layers
  | {
      method: (.http | .. | objects | select(has("http.request.method")) | .["http.request.method"]),
      path:   (.http | .. | objects | select(has("http.request.uri"))    | .["http.request.uri"]),
      json:   (.json."json.object" | fromjson)
    }'

Here, we are filtering values inside ._source.layers.http that contain the http.request.method key using select(has("http.request.method")) and extracting with .["http.request.method"].

Here’s an example of an HTTP request packet output in JSON format (some fields are omitted for simplicity):

{
  "_source": {
    "layers": {
      "http": {
        "POST /books HTTP/1.1\\r\\n": {
          "http.request.method": "POST",
          "http.request.uri": "/books",
          "http.request.version": "HTTP/1.1"
        },
        "http.host": "localhost:8080",
        "http.request.line": "Host: localhost:8080\r\n",
        "http.user_agent": "curl/8.7.1",
        "http.request.line": "User-Agent: curl/8.7.1\r\n",
        "http.accept": "*/*",
        "http.request.line": "Accept: */*\r\n",
        "http.content_type": "application/json",
        "http.request.line": "Content-Type: application/json\r\n",
        "http.content_length_header": "58",
        "http.content_length_header_tree": {
          "http.content_length": "58"
        },
        "http.request.line": "Content-Length: 58\r\n",
        "\\r\\n": "",
        "http.request": "1",
        "http.request.full_uri": "http://localhost:8080/books",
      },
      "json": {
        "json.object": "{\"title\":\"Sample Book\",\"author\":\"John Doe\"}"
      }
    }
  }
}

There’s obviously more you could do, but this should give you a good overview of how to debug your HTTP traffic with tshark after capturing it with tcpdump. LLMs are also pretty good at this if you need more complex queries and analysis.

Cheat Sheet

Finally, here’s a cheat sheet of what I’ve covered in this article, just in case you want to refer to it and dump it into your LLM as a reference:

GoalCommand
Capture traffic on port 8080sudo tcpdump -i any port 8080
Capture traffic and save to filesudo tcpdump -i any port 8080 -w output.pcap
Generate test trafficcurl localhost:8080/books
Read captured packetstshark -r output.pcap
Show only HTTP packetstshark -r output.pcap -Y 'http'
Show selected HTTP fieldstshark -r output.pcap -Y 'http' -T fields -e tcp.stream -e frame.time -e http.request.method -e http.request.uri -e http.response.code
Find available HTTP fieldstshark -G fields | rg "http\."
Filter HTTP 200 responsestshark -r output.pcap -Y 'http.response.code == 200' -T fields -e tcp.stream -e frame.time -e http.request.method -e http.request.uri -e http.response.code
Filter one TCP streamtshark -r output.pcap -Y 'tcp.stream == 9 and http'
Extract JSON response bodytshark -r output.pcap -Y 'http.response.code == 200' -T json 2>/dev/null | jq -r '.[]._source.layers.json."json.object"'
Extract JSON request bodytshark -r output.pcap -Y 'http.request' -T json 2>/dev/null | jq '.[] | ._source.layers | { method: (.http | .. | objects | select(has("http.request.method")) | .["http.request.method"]), path: (.http | .. | objects | select(has("http.request.uri")) | .["http.request.uri"]), json: (.json."json.object" | fromjson) }'

Conclusion

That’s it. I used to think using tcpdump and analyzing HTTP traffic with tools like Wireshark or tshark was hard. But after needing them to investigate a production incident (with customer approval in their test environment), I realized they’re very approachable once you know the basics, which I hope this article provides.

Not to mention, those basics are now easier to pick up than ever. It’s literally just a few questions away with your LLM.

That said, LLMs can still be wrong, so always verify results manually.


  1. You can also capture traffic with tshark, but tcpdump is often the better choice on production or remote machines where installing a full Wireshark toolchain is less practical. ↩︎