URL Encoding in OAuth2 — Tracing the Redirect URI Through Registration, Request, and Callback
The error message is infuriatingly vague:
invalid_request: redirect_uri_mismatch
Or the slightly more helpful:
The redirect URI is not registered for this client
You check your registered URI in the OAuth provider dashboard. It matches your callback URL character-for-character. You refresh. Try again. Same error.
Most OAuth2 redirect URI failures are encoding problems. The URI you registered does not match the URI the provider sees because somewhere between configuration, request, and callback, the encoding transformed it.
This guide traces URL encoding through the full OAuth2 authorization code flow, showing exactly where encoding matters and why it causes the failures it does. If you want to inspect how your URIs are actually encoded, the URL Encoder/Decoder shows the percent-encoded representation for any input.
The OAuth2 Redirect Pipeline
The redirect URI passes through four distinct stages, each with its own encoding rules:
- Registration: You register a redirect URI with the provider
- Authorization request: You send the URI as a query parameter
- Provider interpretation: The provider decodes and compares values
- Browser processing: The browser navigates to the redirect URL
An encoding mismatch at any stage causes redirect_uri_mismatch.
Stage 1: URI Registration
When you register a redirect URI, the provider stores it exactly as submitted. If your dashboard shows:
https://myapp.com/auth/callback?source=web
But your application actually redirects to:
https://myapp.com/auth/callback?source=web&client=mobile
The mismatch happens because the registered URI has source=web as a static query parameter, while the actual redirect dynamically adds parameters.
Common Registration Encoding Mistakes
Trailing slashes: https://myapp.com/callback/ vs https://myapp.com/callback are different URIs.
Query parameter order: ?a=1&b=2 vs ?b=2&a=1 are different strings.
Case sensitivity: The scheme and host are case-insensitive, but the path and query are case-sensitive. https://MyApp.com/Callback is not the same as https://myapp.com/callback.
URL encoding in registration: Some providers normalize registered URIs; others store them literally. If you register https://myapp.com/callback?redirect=https%3A%2F%2Fexample.com, the provider might decode it before comparison.
Stage 2: The Authorization Request
The authorization request is a front-channel redirect from the user's browser to the OAuth provider:
https://auth.provider.com/authorize?
response_type=code&
client_id=abc123&
redirect_uri=https%3A%2F%2Fmyapp.com%2Fauth%2Fcallback&
state=xyz789&
scope=openid%20profile
The redirect_uri value here is percent-encoded because it is a query parameter value. If your redirect URI contains special characters — spaces, ampersands, hash symbols — they must be encoded:
// Correct: encode the entire URI as a query parameter value
const params = new URLSearchParams({
response_type: 'code',
client_id: 'abc123',
redirect_uri: 'https://myapp.com/auth/callback?source=web',
state: crypto.randomUUID(),
scope: 'openid profile',
});
const authUrl = `https://auth.provider.com/authorize?${params.toString()}`;
// authUrl uses URLSearchParams which auto-encodes all values
This is the most common mistake. Developers build the URL manually:
// WRONG: manual string interpolation skips encoding
const authUrl = `https://auth.provider.com/authorize?response_type=code&redirect_uri=${redirectUri}`;
If redirectUri contains an ampersand (like https://myapp.com/callback?source=web), the &source=web part is interpreted as separate query parameters, not part of the redirect URI value.
Stage 3: Provider Comparison
The provider receives the authorization request and decodes the query parameters. It then compares the decoded redirect_uri parameter against the registered URI.
The comparison logic varies between providers:
- Exact match: The decoded parameter must be byte-for-byte identical to the registered URI
- Prefix match: The decoded parameter must start with the registered URI (Google uses this)
- Pattern match: The registered URI can contain wildcards (Auth0 supports this)
Encoding-Discrepancy Failures
If your application sends:
redirect_uri=https://myapp.com/callback?redirect=https%3A%2F%2Fother.com
The provider decodes this to:
redirect_uri = https://myapp.com/callback?redirect=https://other.com
If the registered URI is https://myapp.com/callback?redirect=https%3A%2F%2Fother.com (with the encoding preserved), comparison fails because the provider decoded the parameter before comparing.
The fix is to register the URI in its decoded form and let the provider's comparison logic handle encoding, or double-encode the nested URL if the provider stores the registered URI literally.
Stage 4: Browser Callback
After authorization, the provider redirects the browser to:
https://myapp.com/callback?code=AUTH_CODE_123&state=xyz789
The browser interprets this URL as-is. If your redirect URI registered as:
https://myapp.com/callback?return_url=https://myapp.com/dashboard
The provider redirects to:
https://myapp.com/callback?code=...&state=...&return_url=https://myapp.com/dashboard
This works because the provider appends query parameters to the existing ones. But if your return_url value contains special characters:
https://myapp.com/callback?return_url=https://myapp.com/path?page=1&lang=en
The &lang=en becomes a separate parameter in the provider's redirect unless the provider percent-encodes the appended value. Not all providers do this consistently.
Encoding Scenarios and Solutions
Scenario A: Static Redirect URI
Problem: redirect_uri_mismatch despite identical URIs.
Check: Trailing slash, case sensitivity, query parameter order.
Fix: Copy the exact URI from the provider dashboard into your application configuration. Do not retype it.
Scenario B: Redirect URI with Query Parameters
Problem: Your redirect URI includes ?source=web&utm_campaign=signup but the provider fails to match.
Check: The provider may decode the parameter value before comparison, turning %26 into & and breaking the URI structure.
Fix: Register the decoded form https://myapp.com/callback?source=web&utm_campaign=signup if the provider supports query parameters in redirect URIs. Some providers explicitly disallow this — check their documentation.
Scenario C: Dynamically Constructed Redirect URI
Problem: Your application constructs the redirect URI at runtime with URLSearchParams or manual string building, but the encoding is inconsistent.
Fix: Use URL and URLSearchParams consistently at every layer:
function buildRedirectUri(basePath, params) {
const url = new URL(basePath, 'https://myapp.com');
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
return url.toString();
}
function buildAuthRequest(redirectUri) {
const params = new URLSearchParams({
response_type: 'code',
client_id: 'abc123',
redirect_uri: redirectUri,
state: crypto.randomUUID(),
});
return `https://auth.provider.com/authorize?${params.toString()}`;
}
Using URL and URLSearchParams ensures encoding is consistent and spec-compliant at every stage.
Scenario D: Nested URLs Inside Redirect URIs
Problem: Your redirect URI contains another URL as a query parameter value:
https://myapp.com/callback?next=https://myapp.com/dashboard?theme=dark
Check: The inner ?theme=dark is interpreted as separate query parameters by naive parsing.
Fix: Encode the inner URL:
const innerUrl = 'https://myapp.com/dashboard?theme=dark';
const redirectUri = `https://myapp.com/callback?next=${encodeURIComponent(innerUrl)}`;
Result: https://myapp.com/callback?next=https%3A%2F%2Fmyapp.com%2Fdashboard%3Ftheme%3Ddark
Testing Your Encoding
Before debugging a redirect_uri_mismatch for hours, verify what bytes the provider actually receives:
- Log the constructed authorization URL
- Copy it into the URL Encoder/Decoder
- Inspect the decoded query parameters
- Verify the
redirect_uriparameter decodes to exactly your registered URI
// Debugging helper
function debugRedirectUri(registeredUri, constructedUrl) {
const parsed = new URL(constructedUrl);
const sentUri = parsed.searchParams.get('redirect_uri');
console.log('Registered:', registeredUri);
console.log('Sent (encoded):', sentUri);
console.log('Sent (decoded):', decodeURIComponent(sentUri));
console.log('Match:', registeredUri === decodeURIComponent(sentUri));
}
Related Resources
For more on the encoding issues that cause OAuth redirect failures:
- Double URL Encoding — How It Happens and Why It Breaks Your API Requests
- Common URL Encoding Mistakes Developers Keep Making Common Mistakes
- How to Fix Invalid URL Encoding Errors in APIs
- encodeURI vs encodeURIComponent — What's the Real Difference
FAQ
Why does my OAuth redirect work in development but fail in production?
Development often uses localhost or 127.0.0.1 without query parameters. Production redirects commonly include source tracking, environment identifiers, or dynamic return paths that introduce encoding-sensitive characters.
Should I URL-encode the redirect URI in the authorization request?
Yes. The redirect URI is a query parameter value and must be percent-encoded. Use URLSearchParams or encodeURIComponent() to encode it properly.
Why do some providers accept trailing slashes and others don't?
Providers differ in their comparison logic. Some normalize before comparison; others compare byte-for-byte. Always check the provider's documentation and test with their exact expected format.
Can I register multiple redirect URIs?
Most providers support multiple URIs. Register all variants you use — with and without trailing slash, with and without specific query parameters — to avoid mismatch errors.
What is double encoding and how does it cause OAuth failures?
Double encoding happens when an already encoded value is encoded again. %20 becomes %2520. If the provider expects a decoded value but receives a double-encoded one, comparison fails. This commonly occurs when developers manually encode a value that gets encoded again by URLSearchParams.
Final Thoughts
OAuth2 redirect URI failures are almost never about authentication logic. They are about byte-level string comparison after multiple layers of encoding, decoding, and transport. The URI you registered, the URI your application constructed, and the URI the provider decoded must all be identical at the byte level.
The most reliable approach is to minimize complexity. Use a simple redirect URI without query parameters whenever possible. Append state and return paths as separate mechanisms — the OAuth state parameter for cross-request integrity, and a short-lived session store for return paths.
When encoding issues do arise, trace the URI through each stage of the pipeline. The URL Encoder/Decoder shows exact percent-encoded representations. The Base64 Encoder & Decoder helps when nonce or state values use Base64 encoding. And the Regex Tester is useful for validating URI patterns before they reach production.