Cloudflare Workers
Resolve deployed Redirections.app rules from an existing Cloudflare Worker without overriding successful application routes.
Overview
This integration checks Redirections.app only after your application returns a 404. Existing pages keep their normal response, while deployed redirect rules can recover legacy URLs at the edge.
The example is fail-open: a lookup miss, timeout, invalid response, or API error returns the original application 404.
Configure Wrangler
Add the project ID as a non-secret variable in wrangler.jsonc:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "your-worker",
"main": "src/index.ts",
"compatibility_date": "2026-07-10",
"vars": {
"REDIRECTIONS_PROJECT_ID": "YOUR_PROJECT_ID",
},
}Store the API key as a Worker secret:
pnpm exec wrangler secret put REDIRECTIONS_API_KEYFor local type generation, place a non-production placeholder in the ignored .dev.vars file, then regenerate bindings:
REDIRECTIONS_API_KEY=local-development-placeholderpnpm exec wrangler typesNever put the API key in vars, source code, or a VITE_/PUBLIC_ variable.
Those values can be exposed to clients or version control.
Add the lookup wrapper
Wrap your existing application handler. Replace app.fetch(...) with your framework or origin handler.
import app from './app'
type LookupResult = {
destination: string
statusCode: 301 | 302
}
async function withRedirections(
request: Request,
env: Cloudflare.Env,
next: () => Promise<Response>,
): Promise<Response> {
const applicationResponse = await next()
const source = new URL(request.url)
const acceptsHtml =
!request.headers.get('accept') ||
request.headers.get('accept')!.includes('text/html') ||
request.headers.get('accept')!.includes('*/*')
if (
applicationResponse.status !== 404 ||
!['GET', 'HEAD'].includes(request.method) ||
!acceptsHtml ||
/\.[a-z0-9]{1,10}$/i.test(source.pathname)
) {
return applicationResponse
}
const lookup = new URL('https://api.3xx.app/v1/lookup')
lookup.searchParams.set('project', env.REDIRECTIONS_PROJECT_ID)
lookup.searchParams.set('path', source.pathname)
try {
const response = await fetch(lookup, {
headers: { 'X-API-Key': env.REDIRECTIONS_API_KEY },
signal: AbortSignal.timeout(250),
})
if (response.status !== 200) return applicationResponse
const data: unknown = await response.json()
if (!isLookupResult(data)) return applicationResponse
const destination = new URL(data.destination, source)
if (!['http:', 'https:'].includes(destination.protocol)) {
return applicationResponse
}
if (!destination.search && source.search) destination.search = source.search
if (destination.href === source.href) return applicationResponse
return new Response(null, {
status: data.statusCode,
headers: {
Location: destination.href,
'Cache-Control':
data.statusCode === 301 ? 'public, max-age=86400' : 'no-store',
},
})
} catch {
return applicationResponse
}
}
function isLookupResult(value: unknown): value is LookupResult {
if (!value || typeof value !== 'object') return false
const candidate = value as Record<string, unknown>
return (
typeof candidate.destination === 'string' &&
(candidate.statusCode === 301 || candidate.statusCode === 302)
)
}
export default {
fetch(request, env, ctx) {
return withRedirections(request, env, () => app.fetch(request, env, ctx))
},
} satisfies ExportedHandler<Cloudflare.Env>The lookup sends only pathname. Incoming query parameters are copied to the destination only when the rule does not define its own query string.
Verify the deployment
Deploy a test rule, then check both a hit and a miss:
curl -I https://www.example.com/legacy-path
curl -I https://www.example.com/intentionally-missingThe hit should return the rule's 301 or 302 response. The miss should retain your application's original 404.
Operational guidance
- Exclude static assets and non-document requests to protect lookup quota.
- Keep the timeout short and preserve the original response on failure.
- Point legacy rules directly to final 200 URLs to avoid redirect chains.
- Use separate projects and API keys for staging and production.
- Run
wrangler types --checkin CI after changing bindings or variables.
Cloudflare service bindings work only between Workers in the same Cloudflare
account. Standard Redirections.app customers should use the authenticated
https://api.3xx.app endpoint shown above.