Accepting HTTP requests without a framework
For quickly debugging an incoming webhook locally, you do not need a full HTTP server. A minimal webhook receiver with netcat and a Bash loop accepts requests, shows headers and body, and answers with a valid HTTP response, in a few lines and with no dependencies.
Table of Contents
- 1. Why a webhook receiver with netcat makes sense
- 2. Netcat basics: nc -l, ports and variants
- 3. Building a simple HTTP receiver with a while loop
- 4. Writing back correct HTTP response headers
- 5. Reading Content-Length and extracting the payload
- 6. A persistent receiver with systemd instead of a one-off run
- 7. Security: why netcat is never production ready
- 8. Debugging incoming webhooks from GitHub, Stripe and GitLab locally
- 9. netcat vs. socat vs. a mini server compared
- 10. Summary
- 11. FAQ
1. Why a webhook receiver with netcat makes sense
Before a webhook gets integrated into an application in production, you first need to clarify what the sender actually transmits: which headers, which content type, which JSON schema, which signature. A full-blown web server with routing and middleware is overkill for this phase. A minimal webhook receiver with netcat answers exactly this question in seconds, without setting up a project or opening a port in an existing application.
netcat, or nc for short, is available on practically every Linux system and opens a TCP listener on any port with a single call. Combined with a Bash loop, that becomes a webhook receiver with netcat that shows every incoming request in full, including all the headers many webhook providers use for signature verification. That is especially valuable when a third-party provider's documentation is incomplete and you need to know what actually comes over the wire.
Combined with a tunneling service like ngrok or cloudflared, such a receiver is also useful: the tunnel forwards the public webhook request to the local netcat listener, and you see in real time what an external service actually sends, before writing a single line of application code.
2. Netcat basics: nc -l, ports and variants
The base command for a webhook receiver with netcat is nc -l -p 8080 in the traditional variant (Debian/Ubuntu, package netcat-traditional) or nc -l 8080 in the OpenBSD variant, which is the default on many other distributions and macOS. The difference in option syntax is one of the most common pitfalls: -p for the port exists only in the traditional variant, while OpenBSD nc expects the port as a positional argument.
A single nc -l call accepts exactly one connection and then exits. For a persistent webhook receiver with netcat, you therefore need an infinite loop that restarts nc after every connection. The option -k (keep listening) exists in some nc variants but behaves inconsistently across implementations, which is why an explicit Bash loop is the more portable and more predictable solution.
#!/usr/bin/env bash
# detect-netcat.sh — identify which netcat variant is installed
set -euo pipefail
if nc -h 2>&1 | grep -q "\-\-version"; then
echo "[INFO] Likely OpenBSD netcat: use 'nc -l PORT'"
elif nc -h 2>&1 | grep -q "\-p port"; then
echo "[INFO] Likely traditional netcat: use 'nc -l -p PORT'"
else
echo "[WARN] Could not reliably detect nc variant, test both syntaxes"
fi
nc -h 2>&1 | head -n 3
3. Building a simple HTTP receiver with a while loop
A webhook receiver with netcat needs two building blocks: a listener that accepts requests, and a fixed HTTP response that gets written back as soon as the request has arrived. The simplest form reads the entire request into a file, returns a minimal 200 OK response, and restarts the listener afterward in a loop.
The order matters: nc must write the response before the connection closes, otherwise the webhook sender waits for a reply that never comes and reports a failure after its own timeout. With tee, the incoming request can be logged and evaluated for the response logic at the same time.
#!/usr/bin/env bash
# webhook-receiver.sh — minimal webhook receiver built on netcat
set -euo pipefail
PORT="${1:-8080}"
LOG_DIR="/tmp/webhook-logs"
mkdir -p "$LOG_DIR"
echo "[INFO] Listening for webhooks on port $PORT (Ctrl+C to stop)"
while true; do
request_file="$LOG_DIR/request-$(date +%s%N).log"
# Capture the raw request, then send a fixed 200 OK response
{
printf 'HTTP/1.1 200 OK\r\n'
printf 'Content-Type: text/plain\r\n'
printf 'Content-Length: 2\r\n'
printf 'Connection: close\r\n'
printf '\r\n'
printf 'OK'
} | nc -l -p "$PORT" -q 1 > "$request_file" 2>/dev/null || true
echo "[INFO] Request captured: $request_file"
head -n 5 "$request_file"
echo "---"
done
This script is deliberately minimal: a webhook receiver with netcat for debugging purposes does not need any real routing logic, it is enough to make the request visible. The option -q 1 tells nc to close the connection one second after input ends, which prevents the process from waiting forever for more data.
4. Writing back correct HTTP response headers
Many webhook senders check the HTTP status code of the response and retry on anything other than 2xx. A webhook receiver with netcat that only writes back raw data without valid HTTP headers is therefore interpreted by the sender as a failure, even if the request was received correctly. The response must begin with the status line, followed by the headers, a blank line, and optionally a body, each ending with CRLF (\r\n), not plain \n.
The Content-Length header must exactly match the byte length of the response body, otherwise some HTTP clients wait for more bytes until their own timeout kicks in. Anyone who wants to test how a webhook provider reacts to specific status codes can simply parameterize this value in the script and deliberately try out different responses (200, 400, 500).
#!/usr/bin/env bash
# webhook-receiver-status.sh — reply with a configurable HTTP status
set -euo pipefail
PORT="${1:-8080}"
STATUS="${2:-200 OK}"
BODY="${3:-{"received":true}}"
BODY_LENGTH=${#BODY}
response=$(printf 'HTTP/1.1 %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s' "$STATUS" "$BODY_LENGTH" "$BODY")
echo "[INFO] Replying with: HTTP/1.1 $STATUS"
printf '%s' "$response" | nc -l -p "$PORT" -q 1
5. Reading Content-Length and extracting the payload
A received webhook request consists of a status line, headers, a blank line, and the actual body, usually JSON. To cleanly extract the body from the recorded file, you find the first blank line and print everything after it. In Bash, awk handles that reliably in a single statement, regardless of how many headers the sender sends along.
The Content-Length header specifies how many bytes follow in the body. For a webhook receiver with netcat this matters because TCP has no concept of message boundaries: without Content-Length, you cannot be sure whether the full body has already arrived or the client is still sending more data. For debugging purposes, it is usually enough to log the value and compare it to the actual body length.
#!/usr/bin/env bash
# parse-webhook.sh — extract headers, content-length, and JSON body
set -euo pipefail
REQUEST_FILE="${1:?Usage: parse-webhook.sh <request-file>}"
# Everything before the first blank line is headers
headers=$(awk '/^\r?$/{exit} {print}' "$REQUEST_FILE")
content_length=$(echo "$headers" | grep -i '^Content-Length:' | awk '{print $2}' | tr -d '\r')
# Everything after the first blank line is the body
body=$(awk 'BEGIN{found=0} /^\r?$/{found=1; next} found{print}' "$REQUEST_FILE")
echo "[INFO] Declared Content-Length: ${content_length:-unknown}"
echo "[INFO] Actual body length: ${#body}"
if command -v jq &>/dev/null && [[ -n "$body" ]]; then
echo "$body" | jq . 2>/dev/null || echo "[WARN] Body is not valid JSON"
fi
6. A persistent receiver with systemd instead of a one-off run
For occasional debugging, a script you start manually in the terminal is enough. But if a webhook receiver with netcat needs to run on a test or staging server for several days, a systemd service is the more robust solution: it restarts automatically after a crash, runs in the background, and writes logs via journalctl instead of into a loose text file.
The unit file simply calls the receiver script with Restart=always. That prevents a single faulty connection that crashes nc from permanently disabling the whole debugging receiver. For more production-like purposes, this approach should still remain only temporary, see section seven on security limits.
| Tool | Setup Effort | HTTP Compliance | Suited For |
|---|---|---|---|
| netcat + while loop | Minimal, no setup | Only what you write yourself | Quick look at raw payloads |
| socat | Low | Somewhat more robust than nc | Several simultaneous connections |
| Python http.server | Medium | Fully compliant | More realistic application behavior |
| ngrok + local app | Higher | Fully compliant | End-to-end test with a real application |
7. Security: why netcat is never production ready
A webhook receiver with netcat is a debugging tool, not a production service. It lacks any signature verification, TLS termination, rate limiting, and error handling for parallel connections. A single nc process, in its simplest form, can only ever handle one connection at a time, which leads to lost requests when several webhooks arrive simultaneously, without the sender noticing.
More importantly: without checking the signature that most webhook providers send along in a header (such as X-Hub-Signature-256 for GitHub or Stripe-Signature for Stripe), such a receiver accepts any request, including forged ones. For local debugging that does not matter, but for any production use, a webhook receiver with netcat is fundamentally unsuitable and should never be reachable beyond your own developer network.
Mironsoft
Shell automation, webhook integrations and deployment infrastructure
Reliably receiving and processing webhooks?
We build production-ready webhook receivers with signature verification, retry logic and monitoring, and help debug third-party integrations locally for your Magento store.
Debugging Tools
Minimal receivers for quick analysis of incoming webhook payloads
Production Integration
Signature verification, idempotency and error handling for real webhook endpoints
Monitoring
Alerting on failed or missing webhooks
8. Debugging incoming webhooks from GitHub, Stripe and GitLab locally
The three most common webhook sources in a developer's daily work are GitHub (push and pull request events), GitLab (pipeline and merge request events), and Stripe (payment events). All three send structured JSON with a signature in a header. A webhook receiver with netcat, combined with a tunneling tool like ngrok, shows exactly which fields actually appear in the payload, often differing from outdated documentation examples.
This is especially helpful with Stripe, where event schemas can differ between API versions. Instead of developing against the production API, you configure the webhook to point, for testing, at the local ngrok URL that reaches the netcat receiver, and see every sent event with its full body in real time. That significantly shortens integration time because you do not have to jump back and forth between dashboard, logs, and code.
9. netcat vs. socat vs. a mini server compared
For simply looking at raw data, netcat remains the fastest option because no additional package needs to be installed. As soon as multiple connections need to arrive simultaneously, for example in load tests with several simulated webhook senders, socat with its more flexible fork options is the better choice, because it processes connections in parallel rather than one after another.
A small Python server with http.server or a tiny Node application delivers full HTTP compliance including correct chunked encoding and HTTP/1.1 keep-alive, which a webhook receiver with netcat does not offer. For the very first visual check of an unknown payload, netcat nonetheless remains the most pragmatic entry point, since no additional code needs to be written.
10. Summary
A minimal webhook receiver with netcat solves a very concrete problem: quickly seeing, without a framework, what a webhook sender actually sends. A while loop around nc -l accepts requests, a fixed or parameterized HTTP response with correct status code, Content-Length, and CRLF line endings keeps the sender happy, and awk cleanly extracts headers and body from the recorded file.
For ongoing debugging over several days, a systemd service makes the receiver robust against crashes. The limit remains important: a webhook receiver with netcat never belongs in production, because signature verification, TLS, and parallel processing are missing. For quick local analysis and for debugging third-party integrations, it is nonetheless one of the most useful Bash tools in everyday networking work.
Minimal Webhook Receiver with netcat — The Essentials at a Glance
Listener
nc -l -p PORT in a while loop, so the receiver keeps running after every request.
HTTP Response
Status line, headers, blank line, body, each with CRLF, plus an exact Content-Length.
Payload Parsing
awk splits headers and body at the first blank line, jq validates JSON.
Limits
No production use: no signature verification, no TLS, no real parallel processing.