Follow a URL through every redirect hop — status codes, timing, TLS, cookies, and built-in security checks.
Single file · Zero dependencies · Standard library only
- Why
- Features
- Example Output
- Installation
- Usage
- Extending the Shortener/Tracker List
- JSON Output
- Exit Codes
- Design Notes
- Contributing
- License
Shortened and redirected links hide what they actually do. Redirect Detector unwinds the whole chain so you can see it:
- 🌍 Where a link really goes
- 🖥️ What infrastructure serves each hop
- 🔓 Whether it silently drops encryption
⚠️ Whether anything about the chain looks off — before you (or someone you sent it to) follows it
- HTTP status code with a human-readable label
- HTTP method, and how it changes across redirect types (
301/302/303→ GET,307/308preserve method + body) - Resolved IP address, port, and
Serverheader for the actual backend that answered - Timing breakdown: DNS lookup / TCP+TLS connect / time-to-first-byte
- TLS certificate issuer and expiry on HTTPS hops
- Cookies set at each hop
- Known URL-shortener / tracker-domain labeling (
bit.ly,t.co,doubleclick.net, …) — user-extensible Retry-Afterguidance on429/503responses
| Notice | What it catches |
|---|---|
| 🔓 Downgrade | HTTPS → HTTP mid-chain |
| 🔁 Loop | Redirect loops |
| 🎯 Internal IP | Redirects to private/loopback/link-local IPs or cloud metadata (169.254.169.254) — a common SSRF/redirect-abuse pattern |
| 📜 Weak TLS | Expired certificates, or expiring within 14 days |
| 🍪 Unsecured cookies | Cookies missing the Secure flag over HTTPS, or missing HttpOnly |
| ➰ Long chains | Unusually long redirect chains — a common cloaking/phishing signal |
| Redirects that hit the configured hop limit before resolving |
- One-line chain diagram (
host ─301─▶ host ─302─▶ host) - Redirect-type breakdown (
301vs302vs307/308, etc.) - Every unique domain, IP, and server seen in the chain
- An equivalent
curlcommand for reproducing the initial request by hand - Best-effort HTTP/2 (ALPN) support probe on the final destination — run as a fully isolated side probe, so a middlebox that mishandles the
h2ALPN offer can never break the main trace
--json— machine-readable output for scripting, CI, and piping intojq--diff-user-agent— run the trace twice with different User-Agents and report where the chains diverge (catches mobile-vs-desktop or bot-vs-browser redirect branching)--method/--data— trace a non-GET initial request (e.g. where a webhookPOSTends up)--insecure— skip TLS verification against self-signed or internal endpoints--output FILE— write the trace straight to disk
🔍 Tracing redirects for: http://bit.ly/example
────────────────────────────────────────────────────────────────────────────
Hop 1: GET http://bit.ly/example
→ 301 redirect to: https://example.com/landing?utm_source=bitly
Hop 2: GET https://example.com/landing?utm_source=bitly
→ 200 (94ms)
REDIRECT TRACE
────────────────────────────────────────────────────────────────────────────
├─ Hop 1 [301] GET 118ms [shortener]
│ http://bit.ly/example
│ Moved Permanently → following redirect
│ Host bit.ly:80
│ IP 67.199.248.10
│ Time dns 12ms / connect 19ms / ttfb 60ms
│
└─ Hop 2 [200] GET 94ms
https://example.com/landing?utm_source=bitly
OK — Final destination
Host example.com:443
IP 93.184.216.34
TLS DigiCert Inc (expires Nov 03 12:00:00 2026 GMT)
Time dns 4ms / connect 8ms / tls 22ms / ttfb 41ms
Cookie session_id
✓ FINAL DESTINATION REACHED
Total hops: 2
Final status: 200
Final URL: https://example.com/landing?utm_source=bitly
Protocol: HTTP/2
Total time: 212ms
Chain:
bit.ly ─301─▶ example.com
ℹ Notes:
• hop 1: bit.ly is a known shortener
- Python 3.8+ — that's it.
- No third-party dependencies. Everything runs on the standard library (
urllib,http.client,ssl,socket). No venv, nopip install, no lockfile.
curl -O https://raw.githubusercontent.com/Lunatic16/redirect-detector/main/redirect_detector.py
chmod +x redirect_detector.pyTip
Drop it somewhere on your $PATH to run it as redirect-detector from anywhere:
mv redirect_detector.py ~/.local/bin/redirect-detectorpython3 redirect_detector.py <url> [options]If you omit the scheme, the tool probes both https:// and http:// and picks whichever responds:
python3 redirect_detector.py bit.ly/example| Flag | Short | Description |
|---|---|---|
--headers |
-H |
Show response headers for each hop |
--timeout SECONDS |
-t |
Request timeout per hop (default: 15) |
--user-agent UA |
-a |
Custom User-Agent string |
--max-hops N |
-m |
Maximum redirects to follow before stopping (default: 30) |
--no-ip |
Skip DNS resolution / IP display for a faster trace | |
--json |
Machine-readable JSON output instead of the formatted trace | |
--no-color |
Disable ANSI colors | |
--curl |
Print an equivalent curl command for the initial request |
|
--no-http2-probe |
Skip the best-effort HTTP/2 probe on the final URL | |
--diff-user-agent UA |
Re-run the trace with a second User-Agent and report where the chains diverge | |
--method METHOD |
-X |
HTTP method for the initial request (default: GET) |
--data BODY |
-d |
Request body for the initial request (implies POST if --method isn't set); dropped on a 301/302/303 method-reset, carried through 307/308 |
--insecure |
-k |
Skip TLS certificate verification |
--output FILE |
-o |
Write the trace (formatted or --json) to a file instead of stdout |
--extra-shorteners DOMAINS |
Comma-separated extra domains to treat as shorteners for this run | |
--extra-trackers DOMAINS |
Comma-separated extra domains to treat as trackers for this run |
Run --help for the full built-in reference at any time.
# Basic trace
python3 redirect_detector.py https://bit.ly/example
# Show response headers at every hop
python3 redirect_detector.py https://t.co/abc123 --headers
# Skip DNS lookups for a faster trace
python3 redirect_detector.py https://example.com --no-ip
# Machine-readable output, piped into jq
python3 redirect_detector.py https://example.com --json | jq '.final_url'
# Get the equivalent curl command
python3 redirect_detector.py https://example.com --curl
# Compare how a link behaves for two different User-Agents
python3 redirect_detector.py https://example.com --diff-user-agent "curl/8.0"
# Trace where a webhook POST redirect actually ends up
python3 redirect_detector.py https://example.com -X POST --data "event=ping"
# Trace a self-signed internal endpoint
python3 redirect_detector.py https://internal.example.local --insecure
# Save a JSON trace to disk
python3 redirect_detector.py https://example.com --output trace.json --jsonThe built-in shortener and tracker lists cover the common cases but aren't exhaustive. Add your own domains permanently via a config file:
~/.config/redirect_detector/domains.txt
# one entry per line — '#' starts a comment
shortener:mycompany.link
tracker:some-ad-network.example
…or one-off for a single run:
python3 redirect_detector.py https://example.com --extra-shorteners "mycompany.link,short.io"--json emits a single object per trace:
Useful for scripting and CI checks:
| Code | Meaning |
|---|---|
0 |
Final destination reached with a 2xx status |
1 |
Chain resolved, but the final status wasn't 2xx (e.g. ended on a 404) |
2 |
The trace never produced a usable response — connection failure, timeout, or an unparseable URL |
python3 redirect_detector.py https://example.com --no-color >/dev/null 2>&1 \
|| echo "link is broken"- Single file, zero dependencies. Works anywhere Python 3.8+ does — no venv, no
pip install, no lockfile. - Clean stdout/stderr split. Progress logging goes to
stderr;stdoutcarries only the final trace, so--jsonoutput pipes cleanly intojqor a file even while you watch progress live. - Isolated HTTP/2 detection. The HTTP/2 probe is a separate ALPN check against only the final URL, so a middlebox that mishandles the
h2offer can never affect redirect-following itself. - Never trusts a URL to be well-formed. Both the input URL and every
Locationheader along the chain are treated as potentially malformed; parsing failures degrade to a clean error instead of a crash.
Issues and pull requests are welcome! Some ideas that would fit well:
- More domains for the built-in shortener/tracker lists
- Additional security checks (open redirects, redirect chains across suspicious TLDs, …)
- Windows
TERM/color edge cases and bug fixes
Distributed under the MIT License.
{ "requested_url": "http://bit.ly/example", "hops": [ /* one object per hop — status, method, host, ip, port, server, timing, tls, cookies, category, retry_after, ... */ ], "failures": [], "warnings": [], "notes": ["hop 1: bit.ly is a known shortener"], "final_status": 200, "final_url": "https://example.com/landing", "total_hops": 2, "total_ms": 212.4, "redirect_type_breakdown": { "301": 1 }, "final_protocol": "HTTP/2" }