DevDockTools

Base64URL Encoding: When You Actually Need It and Why

Standard base64 breaks in URLs and filenames because of +, /, and = characters. Here's when to use base64url instead and how to convert between them correctly.

By Daniel Agrici5 min read
base64encodingurl encodingjwtdeveloper tools

Base64-encode a value, drop it into a URL query parameter, and roughly one time in three you'll get a broken link — because standard base64's alphabet includes +, /, and =, and all three collide with characters that already mean something in a URL. Base64url exists specifically to fix this, and the fix is small enough that reaching for the wrong variant is a common, avoidable bug.

What's Actually Different

Standard base64 (defined in RFC 4648, section 4) uses the alphabet A-Z, a-z, 0-9, +, /, with = as padding to make the output length a multiple of 4. Base64url (RFC 4648, section 5) is the same scheme with two substitutions and one omission:

| | Standard base64 | Base64url | | --- | --- | --- | | Character at index 62 | + | - | | Character at index 63 | / | _ | | Padding | = (required) | Typically omitted | | Safe in URL path/query | No | Yes | | Safe in filenames | No (/ is a path separator) | Yes | | Safe in HTTP headers unescaped | Mostly, but + can be misinterpreted as a space in some contexts | Yes |

The underlying encoding algorithm — mapping 3 bytes of input to 4 output characters — is identical. Only the last two alphabet symbols and the padding convention change.

Why + and / Specifically Break URLs

In a URL query string, + is a legacy convention for encoding a space (from application/x-www-form-urlencoded), so a base64 string containing + can get silently decoded as a space by form-parsing middleware before it ever reaches your base64 decoder. / is the path segment separator — put a raw base64 string with a / into a URL path segment and you've just added an extra, unintended path boundary. Both are "sometimes works, sometimes doesn't" bugs depending on which layer of your stack touches the string first, which makes them nasty to track down.

The = padding is a smaller problem — it's technically URL-safe on its own — but it's often dropped anyway in base64url implementations because trailing = characters look like key-value delimiters to some naive query-string parsers, and because it's derivable: given the encoded string's length, you always know how much padding it should have had.

Converting Between the Two in JavaScript

Node and browsers don't have a single built-in "encode as base64url" call in every context, so the conversion is usually a small string transform layered on top of standard base64:

function toBase64Url(base64) {
  return base64
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

function fromBase64Url(base64url) {
  let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
  // Restore padding to a multiple of 4
  while (base64.length % 4) {
    base64 += '=';
  }
  return base64;
}

const encoded = toBase64Url(Buffer.from('hello world?').toString('base64'));
// 'aGVsbG8gd29ybGQ_'

const decoded = Buffer.from(fromBase64Url(encoded), 'base64').toString('utf8');
// 'hello world?'

Node's Buffer actually supports 'base64url' as a native encoding directly, which skips the manual transform entirely:

const encoded = Buffer.from('hello world?').toString('base64url');
// 'aGVsbG8gd29ybGQ_'

const decoded = Buffer.from(encoded, 'base64url').toString('utf8');
// 'hello world?'

In the browser, where there's no Buffer, you're working with btoa/atob (which only handle Latin1 strings, so wrap with TextEncoder/TextDecoder for arbitrary UTF-8) and applying the same character substitution manually.

Where This Comes Up in Practice

JWTs. RFC 7519 specifies that JSON Web Tokens use base64url without padding for all three segments — header, payload, signature. This is precisely because a JWT commonly travels in an Authorization header or a URL query parameter, and standard base64's +///= would need extra escaping in both contexts. If you're hand-rolling JWT encoding for any reason, using standard base64 by mistake produces a token that some strict parsers will reject outright.

Signed URLs and short-lived tokens. Password reset links, magic-link auth, and pre-signed download URLs frequently embed an encoded payload directly in the URL. Base64url avoids the need to percent-encode the token afterward, which keeps the URL shorter and avoids double-encoding bugs where a value gets percent-encoded once by your code and again by an HTTP client library.

Filenames derived from content hashes or IDs. If you're encoding a UUID, hash, or binary identifier into a filename, standard base64's / would be interpreted as a directory separator on most filesystems. Base64url sidesteps that entirely.

Non-cases: if the encoded value only ever lives inside a JSON field, a database column, or an email body — never a URL, path, or filename — plain base64 is fine and there's no reason to switch. The decision point is entirely about where the string travels next, not some general preference for one alphabet over the other.

Quick Reference

Default to base64url whenever the encoded output might end up in a URL, filename, or HTTP header — treat plain base64 as the exception, reserved for contexts you know are purely internal (JSON payloads, database blobs). To encode or decode values interactively while debugging a token or signed link, the Base64 Encoder handles standard base64 conversion — just remember to apply the +//-/_ substitution afterward if the target context is a URL.

Next time you're building anything that puts an encoded value into a link — password resets, share links, webhook callback URLs — check which base64 variant you're actually emitting before it ships, not after a user reports a broken link with a stray + turned into a space.

Frequently Asked Questions

What's the actual difference between base64 and base64url?
Base64url replaces the two characters that aren't URL-safe: + becomes -, and / becomes _. It also conventionally omits the = padding characters, since padding is derivable from string length and can conflict with query string delimiters.
Do JWTs use base64 or base64url?
JWTs use base64url without padding for the header, payload, and signature segments, as specified in RFC 7519. Encoding a JWT with standard base64 instead produces a token that breaks when passed as a URL parameter or an Authorization header in some strict parsers.
Can I just percent-encode standard base64 instead of using base64url?
Technically yes, but it defeats the purpose. Percent-encoding + and / makes the string longer and reintroduces characters (%) that need further escaping in some contexts, whereas base64url produces a string that's already safe everywhere without extra encoding.