valuated at the edge (CDN/Edge Functions) rather than the origin. This minimizes latency and reduces load on backend servers.
TypeScript Implementation
The following TypeScript module provides a production-grade redirect auditor. It returns a structured report detailing every hop, status codes, and validation results.
import { fetch } from 'undici'; // Node.js environment
export interface Hop {
url: string;
status: number;
location?: string;
headers: Record<string, string>;
}
export interface RedirectAuditReport {
startUrl: string;
hops: Hop[];
finalUrl: string;
finalStatus: number;
chainLength: number;
isValid: boolean;
errors: string[];
warnings: string[];
}
export interface AuditConfig {
maxHops?: number;
timeoutMs?: number;
followCrossDomain?: boolean;
}
const DEFAULT_CONFIG: Required<AuditConfig> = {
maxHops: 10,
timeoutMs: 5000,
followCrossDomain: true,
};
export async function auditRedirectChain(
targetUrl: string,
config: AuditConfig = {}
): Promise<RedirectAuditReport> {
const opts = { ...DEFAULT_CONFIG, ...config };
const hops: Hop[] = [];
const visitedUrls = new Set<string>();
const errors: string[] = [];
const warnings: string[] = [];
let currentUrl = targetUrl;
let finalStatus = 0;
for (let i = 0; i < opts.maxHops; i++) {
// Cycle detection
if (visitedUrls.has(currentUrl)) {
errors.push(`Infinite loop detected at ${currentUrl}`);
break;
}
visitedUrls.add(currentUrl);
try {
const response = await fetch(currentUrl, {
method: 'HEAD',
redirect: 'manual',
signal: AbortSignal.timeout(opts.timeoutMs),
headers: { 'User-Agent': 'RedirectAuditor/1.0' },
});
const hop: Hop = {
url: currentUrl,
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
};
// Capture Location header for redirects
if (response.status >= 300 && response.status < 400) {
hop.location = response.headers.get('location') || undefined;
}
hops.push(hop);
finalStatus = response.status;
// Handle redirect
if (hop.location) {
// Resolve relative URLs
const nextUrl = new URL(hop.location, currentUrl).href;
// Cross-domain check
if (!opts.followCrossDomain) {
const currentHost = new URL(currentUrl).hostname;
const nextHost = new URL(nextUrl).hostname;
if (currentHost !== nextHost) {
warnings.push(`Cross-domain redirect blocked: ${currentHost} -> ${nextHost}`);
break;
}
}
currentUrl = nextUrl;
continue;
}
// Final response reached
break;
} catch (err) {
errors.push(`Request failed for ${currentUrl}: ${(err as Error).message}`);
break;
}
}
// Validation logic
if (hops.length > opts.maxHops) {
errors.push(`Chain exceeded max hops limit (${opts.maxHops})`);
}
if (finalStatus !== 200 && finalStatus !== 304) {
errors.push(`Final status is ${finalStatus}, expected 200 or 304`);
}
// Check for 302 usage on permanent paths
hops.forEach((hop, idx) => {
if (hop.status === 302) {
warnings.push(`Hop ${idx + 1} uses 302 Found. Ensure this is temporary.`);
}
if (hop.status === 301 && idx < hops.length - 1) {
// 301 in the middle of a chain is often a misconfiguration
warnings.push(`Hop ${idx + 1} uses 301 Moved Permanently mid-chain. Consider consolidating.`);
}
});
return {
startUrl: targetUrl,
hops,
finalUrl: currentUrl,
finalStatus,
chainLength: hops.length,
isValid: errors.length === 0,
errors,
warnings,
};
}
Rationale
HEAD Method: Using HEAD reduces payload transfer during audits, focusing solely on headers and status codes. This speeds up bulk checks.
- Undici/Fetch: Modern fetch implementations provide better control over redirects and timeouts compared to legacy
http modules.
- Relative URL Resolution: The
new URL(hop.location, currentUrl) pattern ensures relative Location headers are handled correctly, a common source of audit failures.
- Warning System: The code distinguishes between errors (broken chains, loops, non-200 finals) and warnings (302 usage, mid-chain 301s). This allows teams to prioritize fixes based on severity.
- Cross-Domain Control: The
followCrossDomain flag enables auditing of external redirects, which is crucial for tracking links and affiliate routing.
Pitfall Guide
Production environments often harbor subtle redirect misconfigurations. The following pitfalls are frequently encountered during audits and migrations.
| Pitfall Name | Explanation | Fix Strategy |
|---|
| Protocol-Host-Slug Fragmentation | Rules are split across layers: one rule handles httpβhttps, another handles wwwβnon-www, and a third handles path changes. This creates a 3-hop chain. | Consolidate rules into a single canonicalization step. Ensure the first redirect resolves protocol, host, and path simultaneously where possible. |
| 302 Masquerading as 301 | Developers use 302 Found for permanent moves due to browser caching behavior or CMS defaults. This prevents search engines from transferring link equity and causes repeated re-crawling. | Audit all redirects. Use 301 Moved Permanently for permanent URL changes. Reserve 302 strictly for temporary campaigns or A/B tests. |
| Infinite Loop Traps | Misconfigured rules cause a URL to redirect to itself or a cycle (e.g., AβBβA). This results in browser errors and crawl failures. | Implement max-hop limits in resolvers. Review rule logic for circular dependencies. Use the cycle detection logic in the audit script. |
| Proxy Injection Hops | Reverse proxies (Nginx, Apache) or CDN edge rules inject redirects that are invisible to the application code. For example, a CDN might force HTTPS before the origin server processes the request. | Map the full infrastructure stack. Audit URLs against the edge endpoint, not just the origin. Check X-Redirect-By headers to identify the source of hops. |
| Analytics Referral Bleed | Redirects that cross domains or involve multiple hops can break the Referer header chain. Traffic may appear as "Direct" in analytics, obscuring campaign performance. | Keep redirects within the same domain where possible. Use 301 for permanent moves to preserve referrer data. Test analytics attribution alongside redirect audits. |
| Parameter Pollution | URLs with tracking parameters (UTM, click IDs) trigger redirects that strip parameters or route to different destinations, causing inconsistent behavior. | Normalize URLs by stripping tracking parameters before routing logic, or ensure the final destination handles parameters gracefully without additional hops. |
| Staging vs. Production Divergence | Redirect rules differ between staging and production environments. A chain that works in staging may break in prod due to different DNS or CDN configurations. | Mirror production redirect configurations in staging. Run the same audit scripts against both environments. Use infrastructure-as-code to ensure parity. |
Production Bundle
Action Checklist
Use this checklist to validate redirect hygiene before deployment and during routine maintenance.
Decision Matrix
Select the appropriate redirect strategy based on the use case and constraints.
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Site Migration | Bulk 301 Redirects | Transfers SEO equity permanently. Signals to crawlers that content has moved. | Low (Config change). High value for SEO retention. |
| A/B Testing | 302 Redirects or Cookie-based Routing | Temporary nature prevents caching of variant URLs. Preserves original URL indexing. | Medium (Requires testing infrastructure). |
| Legacy Cleanup | Consolidate Rules into Single Hop | Reduces latency and crawl waste. Simplifies maintenance. | Medium (Refactoring effort). Long-term performance gain. |
| Tracking Links | Edge Rewrite or Final Page Handling | Avoids redirect hops for marketing links. Preserves referrer data. | Low (Edge config). Improves analytics accuracy. |
| HTTPS Enforcement | HSTS + Edge Redirect | Forces secure connections efficiently. Reduces protocol hops. | Low. Improves security and performance. |
Configuration Template
Below are configuration examples for common platforms. These templates demonstrate consolidated redirect rules that minimize hops.
Nginx Configuration:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
# Consolidate protocol and host canonicalization
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
# Consolidate path migration
location /old-slug {
return 301 /new-slug;
}
# Catch-all for legacy paths
location ~ ^/legacy/(.*) {
return 301 /modern/$1;
}
location / {
proxy_pass http://backend;
}
}
Vercel / Netlify (vercel.json / netlify.toml style):
{
"redirects": [
{
"source": "/old-blog/:path*",
"destination": "/articles/:path*",
"status": 301,
"force": true
},
{
"source": "/campaign-temp",
"destination": "/landing-v2",
"status": 302,
"conditions": {
"cookie": "experiment=true"
}
},
{
"source": "/(.*)",
"has": [
{
"type": "host",
"value": "example.com"
}
],
"destination": "https://www.example.com/$1",
"status": 301
}
]
}
Quick Start Guide
Get your redirect audit pipeline running in under five minutes.
- Initialize Project: Create a new TypeScript project and install dependencies.
mkdir redirect-audit && cd redirect-audit
npm init -y
npm install undici typescript @types/node
npx tsc --init
- Add Auditor Code: Copy the
auditRedirectChain function from the Core Solution into src/auditor.ts.
- Create URL List: Create a
urls.txt file with critical URLs to test.
https://example.com
https://example.com/old-page
http://example.com/legacy
- Run Audit Script: Create
src/run.ts to read URLs and output results.
import { readFileSync } from 'fs';
import { auditRedirectChain } from './auditor';
const urls = readFileSync('urls.txt', 'utf-8').split('\n').filter(Boolean);
async function main() {
for (const url of urls) {
console.log(`\nAuditing: ${url}`);
const report = await auditRedirectChain(url);
console.log(`Chain Length: ${report.chainLength}`);
console.log(`Final Status: ${report.finalStatus}`);
console.log(`Valid: ${report.isValid}`);
if (report.errors.length) console.error('Errors:', report.errors);
if (report.warnings.length) console.warn('Warnings:', report.warnings);
}
}
main();
- Execute: Run the script and review the output.
npx ts-node src/run.ts
Analyze the report for chain lengths, status codes, and warnings. Fix any identified issues in your configuration and re-run to verify.