url-unshortener
v0.1.1
Published
Expand shortened URLs to their final destination. Zero dependencies.
Maintainers
Readme
url-unshortener
Expand shortened URLs to their final destination. Follows the full redirect chain using Node.js built-in HTTP — zero runtime dependencies.
npx unshort https://bit.ly/xyz
# https://example.com/the-real-pageInstall
npm install url-unshortener
# or
bun add url-unshortenerRequires Node.js 18+
Library
expand(url, options?)
Expands a single URL and returns the final destination.
import { expand } from 'url-unshortener'
const result = await expand('https://bit.ly/xyz')
console.log(result.expandedUrl) // 'https://example.com/the-real-page'
console.log(result.statusCode) // 200
console.log(result.redirectChain) // ['https://bit.ly/xyz', ...]expandMany(urls, options?)
Expands multiple URLs concurrently. Always returns one result per input URL — failures are captured in result.error rather than throwing.
import { expandMany } from 'url-unshortener'
const results = await expandMany([
'https://bit.ly/a',
'https://t.co/b',
'https://tinyurl.com/c',
])
for (const result of results) {
if (result.error) {
console.error(`${result.originalUrl} failed: ${result.error}`)
} else {
console.log(`${result.originalUrl} → ${result.expandedUrl}`)
}
}API Reference
ExpandOptions
| Option | Type | Default | Description |
|---|---|---|---|
| timeout | number | 10000 | Request timeout in milliseconds |
| maxRedirects | number | 10 | Maximum redirects to follow |
| userAgent | string | Chrome UA | User-Agent header |
| headers | Record<string, string> | — | Additional request headers |
| allowHttpDowngrade | boolean | true | Allow redirects from https: to http:. Set to false to reject downgrades |
| allowedHosts | string[] | — | If set, only follow redirects to these hostnames. Requests to any other host throw |
| blockPrivateIPs | boolean | true | Block requests to loopback, RFC1918, link-local, and cloud metadata addresses |
| concurrency | number | 5 | Max concurrent expansions (expandMany only) |
ExpandResult
interface ExpandResult {
originalUrl: string // The URL you passed in
expandedUrl: string // Final destination after all redirects
statusCode: number // HTTP status of the final response
redirectChain: string[] // Each intermediate URL (not including final)
error?: string // Set on failure (expandMany only)
}Error types
All errors extend ExpandError which exposes a url property pointing to the URL that caused the failure.
import { ExpandError, TimeoutError, MaxRedirectsError, InvalidUrlError } from 'url-unshortener'
try {
await expand('https://bit.ly/xyz', { timeout: 3000, maxRedirects: 5 })
} catch (err) {
if (err instanceof TimeoutError) console.error('Timed out')
if (err instanceof MaxRedirectsError) console.error('Too many redirects')
if (err instanceof InvalidUrlError) console.error('Bad URL or protocol')
if (err instanceof ExpandError) console.error('Expansion failed:', err.url)
}CLI
npx unshort <url> [url2 ...] [options]
# alias: npx unshort <url> [url2 ...] [options]| Flag | Description |
|---|---|
| --chain | Print each redirect hop |
| --json | Output as JSON |
| --timeout <ms> | Request timeout in ms (default: 10000) |
| --help | Show usage |
Rich output
By default the CLI prints a human-readable summary for each URL:
Original https://bit.ly/xyz
Expanded https://example.com/the-real-page
Status 200 OK
Security ✓ HTTPS
Hops 2 redirects
Query Parameters (3)
utm_source newsletter TRACKING
ref homepage AFFILIATE
page about OTHER- Status is color-coded: green for 2xx, yellow for 3xx, red for 4xx/5xx.
- Security flags
⚠ HTTPif the final URL is not HTTPS. - Hops is only shown when the URL required at least one redirect.
- Query Parameters classifies each param as
TRACKING(UTM, fbclid, gclid, …),AFFILIATE(ref, tag, partner, …), orOTHER. - Very long URLs are truncated in display mode. Use
--jsonto get the full untruncated URL.
Column widths in the summary block adjust based on label length; the example above is representative.
Colors are automatically disabled when output is piped.
Examples
# Expand a single URL
npx unshort https://bit.ly/xyz
# Show the full redirect chain
npx unshort https://bit.ly/xyz --chain
# Expand multiple URLs at once
npx unshort https://bit.ly/a https://t.co/b
# Machine-readable JSON output (full URLs, no truncation)
npx unshort https://bit.ly/xyz --json
# Custom timeout
npx unshort https://bit.ly/xyz --timeout 5000Security
This library makes outbound HTTP/S requests to arbitrary URLs and follows redirects. Treat it as an SSRF primitive — do not pass raw user input to expand() or expandMany() in a server-side context without caller-side controls.
Server-side usage
When expanding user-supplied URLs on a server, apply restrictions:
const result = await expand(userSuppliedUrl, {
allowHttpDowngrade: false, // reject https → http downgrades
allowedHosts: ['bit.ly', 't.co', 'tinyurl.com'], // only known shorteners
timeout: 5000,
maxRedirects: 5,
})What this library does not protect against
- DNS rebinding —
blockPrivateIPsandallowedHostscheck the URL hostname, not the resolved IP. A public hostname that resolves to a private IP at connection time is not blocked. Combine with network-level egress filtering if full SSRF protection is required. - Response body content — the library never reads or saves response bodies (it closes the connection after headers), so shell scripts and other payloads are not downloaded.
HTTPS → HTTP downgrade
By default, redirects from https: to http: are permitted (common in the wild). Set allowHttpDowngrade: false to reject them.
License
MIT — Aman Harsh
