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:
tcpdumptsharkjqpython3or anything that can spin up a web server.
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 808000:13:14.371043 IP6 localhost.63752 > localhost.http-alt: Flags [S], seq 323445184, win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 2380711463 ecr 0,sackOK,eol], length 0
00:13:14.371070 IP6 localhost.63752 > localhost.http-alt: Flags [S], seq 323445184, win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 2380711463 ecr 0,sackOK,eol], length 0
00:13:14.371224 IP6 localhost.http-alt > localhost.63752: Flags [S.], seq 3793703022, ack 323445185, win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 3134725266 ecr 2380711463,sackOK,eol], length 0
00:13:14.371234 IP6 localhost.http-alt > localhost.63752: Flags [S.], seq 3793703022, ack 323445185, win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 3134725266 ecr 2380711463,sackOK,eol], length 0
# ... more logs
00:13:14.372125 IP6 localhost.http-alt > localhost.63752: Flags [.], ack 79, win 6371, options [nop,nop,TS val 3134725267 ecr 2380711464], length 0 00:13:14.372129 IP6 localhost.http-alt > localhost.63752: Flags [.], ack 79, win 6371, options [nop,nop,TS val 3134725267 ecr 2380711464], length 0If 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.pcapIf 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 1 0.000000 ::1 → ::1 TCP 88 61870 → 8080 [SYN] Seq=0 Win=65535 Len=0 MSS=16324 WS=64 TSval=1626974093 TSecr=0 SACK_PERM
... other logs ...
18 0.000914 127.0.0.1 → 127.0.0.1 HTTP 56 HTTP/1.0 404 Not Found
28 0.001073 127.0.0.1 → 127.0.0.1 TCP 56 [TCP Dup ACK 27#1] 8080 → 61871 [ACK] Seq=101 Ack=79 Win=408256 Len=0 TSval=2010179211 TSecr=2798382382
31 0.700058 ::1 → ::1 TCP 64 8080 → 61872 [RST, ACK] Seq=1 Ack=1 Win=0 Len=0
96 1.879874 127.0.0.1 → 127.0.0.1 HTTP 133 GET / HTTP/1.1
185 91.584502 127.0.0.1 → 127.0.0.1 TCP 201 HTTP/1.0 200 OK
252 93.730777 127.0.0.1 → 127.0.0.1 TCP 56 [TCP Dup ACK 251#1] 8080 → 61895 [ACK] Seq=101 Ack=79 Win=408256 Len=0 TSval=566993429 TSecr=4042384569You 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'12 0.000553 127.0.0.1 → 127.0.0.1 HTTP 133 GET / HTTP/1.1
18 0.000914 127.0.0.1 → 127.0.0.1 HTTP 56 HTTP/1.0 404 Not Found
40 0.700393 127.0.0.1 → 127.0.0.1 HTTP 133 GET / HTTP/1.1
47 0.700689 127.0.0.1 → 127.0.0.1 HTTP 56 HTTP/1.0 404 Not Found
67 1.287958 127.0.0.1 → 127.0.0.1 HTTP 133 GET / HTTP/1.1
74 1.288231 127.0.0.1 → 127.0.0.1 HTTP 56 HTTP/1.0 404 Not Found
96 1.879874 127.0.0.1 → 127.0.0.1 HTTP 133 GET / HTTP/1.1
102 1.880078 127.0.0.1 → 127.0.0.1 HTTP 56 HTTP/1.0 404 Not Found
125 90.198950 127.0.0.1 → 127.0.0.1 HTTP 138 GET /books HTTP/1.1
131 90.199257 127.0.0.1 → 127.0.0.1 HTTP/JSON 175 HTTP/1.0 200 OK , JSON (application/json)
151 91.029506 127.0.0.1 → 127.0.0.1 HTTP 138 GET /books HTTP/1.1
159 91.029810 127.0.0.1 → 127.0.0.1 HTTP/JSON 175 HTTP/1.0 200 OK , JSON (application/json)
179 91.584132 127.0.0.1 → 127.0.0.1 HTTP 138 GET /books HTTP/1.1
187 91.584540 127.0.0.1 → 127.0.0.1 HTTP/JSON 175 HTTP/1.0 200 OK , JSON (application/json)
209 92.069515 127.0.0.1 → 127.0.0.1 HTTP 138 GET /books HTTP/1.1
215 92.069889 127.0.0.1 → 127.0.0.1 HTTP/JSON 175 HTTP/1.0 200 OK , JSON (application/json)
237 93.730389 127.0.0.1 → 127.0.0.1 HTTP 133 GET / HTTP/1.1
243 93.730663 127.0.0.1 → 127.0.0.1 HTTP 56 HTTP/1.0 404 Not FoundNot 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 objectWe 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.code1 2026-03-21T23:53:42.961078000+0800 GET /
1 2026-03-21T23:53:42.961439000+0800 / 404
3 2026-03-21T23:53:43.660918000+0800 GET /
3 2026-03-21T23:53:43.661214000+0800 / 404
5 2026-03-21T23:53:44.248483000+0800 GET /
5 2026-03-21T23:53:44.248756000+0800 / 404
7 2026-03-21T23:53:44.840399000+0800 GET /
7 2026-03-21T23:53:44.840603000+0800 / 404
9 2026-03-21T23:55:13.159475000+0800 GET /books
9 2026-03-21T23:55:13.159782000+0800 /books 200
11 2026-03-21T23:55:13.990031000+0800 GET /books
11 2026-03-21T23:55:13.990335000+0800 /books 200
13 2026-03-21T23:55:14.544657000+0800 GET /books
13 2026-03-21T23:55:14.545065000+0800 /books 200
15 2026-03-21T23:55:15.030040000+0800 GET /books
15 2026-03-21T23:55:15.030414000+0800 /books 200
17 2026-03-21T23:55:16.690914000+0800 GET /
17 2026-03-21T23:55:16.691188000+0800 / 404You 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\."... here are some of the fields shown by the command:
F Response http.response FT_BOOLEAN http 0 0x0 true if HTTP response
F Request http.request FT_BOOLEAN http 0 0x0 true if HTTP request
F Response line http.response.line FT_STRING http 0x0
F Request line http.request.line FT_STRING http 0x0
F Request Method http.request.method FT_STRING http 0x0 HTTP Request Method
F Request URI http.request.uri FT_STRING http 0x0 HTTP Request-URI
F Request URI Path http.request.uri.path FT_STRING http 0x0 HTTP Request-URI Path
F Request URI Path Segment http.request.uri.path.segment FT_STRING http 0x0
F Request URI Query http.request.uri.query FT_STRING http 0x0 HTTP Request-URI Query
F Request URI Query Parameter http.request.uri.query.parameter FT_STRING http 0x0 HTTP Request-URI Query Parameter
F Request Version http.request.version FT_STRING http 0x0 HTTP Request HTTP-Version
F Response Version http.response.version FT_STRING http 0x0 HTTP Response HTTP-Version
F Full request URI http.request.full_uri FT_STRING http 0x0 The full requested URI (including host name)
F Status Code http.response.code FT_UINT24 http BASE_DEC 0x0 HTTP Response Status Code
F Status Code Description http.response.code.desc FT_STRING http 0x0 HTTP Response Status Code Description
F Response Phrase http.response.phrase FT_STRING http 0x0 HTTP Response Reason Phrase
F Authorization http.authorization FT_STRING http 0x0 HTTP Authorization header
F Content-Type http.content_type FT_STRING http 0x0 HTTP Content-Type header
F Content-Length http.content_length_header FT_STRING http 0x0 HTTP Content-Length header
F Content length http.content_length FT_UINT64 http BASE_DEC 0x0
F Content-Encoding http.content_encoding FT_STRING http 0x0 HTTP Content-Encoding header
F Transfer-Encoding http.transfer_encoding FT_STRING http 0x0 HTTP Transfer-Encoding header
F User-Agent http.user_agent FT_STRING http 0x0 HTTP User-Agent header
F Host http.host FT_STRING http 0x0 HTTP Host
F Accept http.accept FT_STRING http 0x0 HTTP Accept
F Referer http.referer FT_STRING http 0x0 HTTP Referer
F Accept-Language http.accept_language FT_STRING http 0x0 HTTP Accept Language
F Accept Encoding http.accept_encoding FT_STRING http 0x0 HTTP Accept Encoding
F Date http.date FT_STRING http 0x0 HTTP Date
F Server http.server FT_STRING http 0x0 HTTP Server
F Location http.location FT_STRING http 0x0 HTTP LocationThe -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.code9 2026-03-21T23:55:13.159782000+0800 /books 200
11 2026-03-21T23:55:13.990335000+0800 /books 200
13 2026-03-21T23:55:14.545065000+0800 /books 200
15 2026-03-21T23:55:15.030414000+0800 /books 200Or filter by a specific tcp.stream:
Filter by TCP stream
tshark -r output.pcap -Y 'tcp.stream == 9 and http'125 90.198950 127.0.0.1 → 127.0.0.1 HTTP 138 GET /books HTTP/1.1
131 90.199257 127.0.0.1 → 127.0.0.1 HTTP/JSON 175 HTTP/1.0 200 OK , JSON (application/json)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"'{"books": [{"name": "The Great Gatsby", "author": "F. Scott Fitzgerald"}, {"name": "1984", "author": "George Orwell"}]}
{"books": [{"name": "The Great Gatsby", "author": "F. Scott Fitzgerald"}, {"name": "1984", "author": "George Orwell"}]}
{"books": [{"name": "The Great Gatsby", "author": "F. Scott Fitzgerald"}, {"name": "1984", "author": "George Orwell"}]}
{"books": [{"name": "The Great Gatsby", "author": "F. Scott Fitzgerald"}, {"name": "1984", "author": "George Orwell"}]}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)
}'{
"method": "POST",
"path": "/books",
"json": {
"title": "Sample Book",
"author": "John Doe"
}
}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:
| Goal | Command |
|---|---|
| Capture traffic on port 8080 | sudo tcpdump -i any port 8080 |
| Capture traffic and save to file | sudo tcpdump -i any port 8080 -w output.pcap |
| Generate test traffic | curl localhost:8080/books |
| Read captured packets | tshark -r output.pcap |
| Show only HTTP packets | tshark -r output.pcap -Y 'http' |
| Show selected HTTP fields | 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 |
| Find available HTTP fields | tshark -G fields | rg "http\." |
| Filter HTTP 200 responses | 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 |
| Filter one TCP stream | tshark -r output.pcap -Y 'tcp.stream == 9 and http' |
| Extract JSON response body | tshark -r output.pcap -Y 'http.response.code == 200' -T json 2>/dev/null | jq -r '.[]._source.layers.json."json.object"' |
| Extract JSON request body | 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) }' |
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.
You can also capture traffic with
tshark, buttcpdumpis often the better choice on production or remote machines where installing a full Wireshark toolchain is less practical. ↩︎