La diferencia en una tabla
Ambas variantes están definidas en RFC 4648. La sección 4 describe el Base64 estándar y la sección 5 el alfabeto «URL and Filename safe». Comparten 62 caracteres y el mismo algoritmo; solo difieren los índices 62 y 63.
| Base64 estándar (RFC 4648 §4) | Base64URL (RFC 4648 §5) | |
|---|---|---|
| Índices 0–61 | A–Z a–z 0–9 | igual |
| Índice 62 | + | - |
| Índice 63 | / | _ |
| Relleno | = obligatorio por defecto | normalmente se omite |
| Seguro en la ruta o la consulta de una URL | no | sí |
| Seguro como nombre de archivo | no (/ separa rutas) | sí |
La misma entrada codificada de ambas formas:
| Entrada | Estándar | Base64URL | Base64URL, sin relleno |
|---|---|---|---|
<<???>> | PDw/Pz8+Pg== | PDw_Pz8-Pg== | PDw_Pz8-Pg |
bytes fb ff | +/8= | -_8= | -_8 |
Man | TWFu | TWFu | TWFu |
La última fila recuerda que muchas cadenas se ven idénticas en ambas variantes. Una cadena solo se puede distinguir si contiene +, /, - o _, o si le falta el relleno. Cuando no puedas saberlo, lo seguro es un decodificador que acepte ambos alfabetos.
Una forma rápida de identificar una cadena que te han pasado:
- Contiene
-o_: Base64URL. - Contiene
+o/: Base64 estándar. - Su longitud no es múltiplo de 4 y no tiene
=: sin relleno, lo que casi siempre significa Base64URL. - Contiene a la vez
+y-, o/y_: algo mezcló los alfabetos, o los datos no son Base64. El validador Base64 muestra qué caracteres están fuera de lugar. - Tiene tres partes separadas por puntos que empiezan por
eyJ: un JWT, que por definición es Base64URL.
Por qué existe Base64URL
En una URL, / separa los segmentos de la ruta, + significa espacio en las cadenas de consulta de formularios y = separa claves de valores. El Base64 estándar usa los tres. Ponerlo en una URL obliga a aplicarle codificación por porcentajes, así que ab+/8= se convierte en ab%2B%2F8%3D: más largo, más feo y un paso más en el que equivocarse.
RFC 4648 §5 lo resuelve eligiendo - y _, que son caracteres no reservados en las URLs y válidos en nombres de archivo en todos los sistemas de archivos habituales. La RFC también indica que el relleno puede omitirse cuando se conoce la longitud, ya que el propio = tendría que escaparse como %3D. Más sobre esto en la guía del relleno.
Dónde se usa cada variante
Base64 estándar:
- Adjuntos de correo MIME y cabeceras codificadas (Base64 en el correo electrónico)
- Certificados y claves PEM
- URIs
data:en HTML y CSS - Cabeceras HTTP Basic Auth
- Secrets de Kubernetes y la mayoría de las APIs JSON que transportan archivos
Base64URL, normalmente sin relleno:
- JSON Web Tokens: la cabecera, la carga útil y la firma son Base64URL sin relleno (Base64 en JWT)
- Claves JWK, y JWS y JWE en general (RFC 7515–7518)
- IDs de credenciales y desafíos de WebAuthn
- Valores
code_challengede OAuth PKCE (RFC 7636) - Tokens aleatorios para URLs, como
secrets.token_urlsafe()de Python, que devolvió una cadena de 43 caracteres para 32 bytes aleatorios
Cómo convertir entre ellas
La conversión es pura sustitución de caracteres, y el relleno se trata aparte. Los bytes nunca cambian.
De estándar a URL-safe: cambia + por - y / por _, y quita los = finales.
De URL-safe a estándar: cambia - por + y _ por /, y añade = hasta que la longitud sea múltiplo de 4.
const toUrl = (b64) =>
b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromUrl = (u) => {
const b64 = u.replace(/-/g, "+").replace(/_/g, "/");
return b64 + "=".repeat((4 - (b64.length % 4)) % 4);
};
toUrl("PDw/Pz8+Pg=="); // "PDw_Pz8-Pg"
fromUrl("PDw_Pz8-Pg"); // "PDw/Pz8+Pg=="
La mayoría de los lenguajes incluyen además un codificador directo, así que rara vez necesitas manipular la cadena:
| Lenguaje | Codificador Base64URL | Relleno |
|---|---|---|
| Python | base64.urlsafe_b64encode(b) | se conserva; quítalo con .rstrip(b"=") |
| Node.js | buf.toString("base64url") | se omite |
| Go | base64.RawURLEncoding / base64.URLEncoding | se omite / se conserva |
| Java | Base64.getUrlEncoder().withoutPadding() | se omite |
| PHP | rtrim(strtr(base64_encode($s), '+/', '-_'), '=') | se omite |
Para decodificar, Buffer.from(s, "base64") de Node acepta ambos alfabetos y la falta de relleno. base64.urlsafe_b64decode de Python necesita que antes se restaure el relleno. atob() del navegador rechaza - y _ con un InvalidCharacterError, así que convierte antes o usa Uint8Array.fromBase64 con alphabet: "base64url" donde esté disponible. El código para otros lenguajes está en las páginas de código, por ejemplo Python y Java.
Si solo necesitas cambiar una cadena, Base64 a Base64URL y Base64URL a Base64 lo hacen en el navegador.
Trampas
El + se convierte en espacio
Es el fallo más habitual. Un Base64 estándar puesto sin escapar en una cadena de consulta se estropea con la decodificación de formularios, que lee + como espacio:
from urllib.parse import parse_qs
parse_qs("t=ab+/8=")
# {'t': ['ab /8=']}
new URLSearchParams("t=ab+/8=").get("t");
// "ab /8="
El decodificador posterior falla entonces con el espacio, o se lo salta y devuelve bytes incorrectos. La solución es usar Base64URL, o encodeURIComponent() al construir la URL. Como arreglo rápido en el lado receptor, volver a cambiar los espacios por + suele funcionar, porque Base64 nunca contiene espacios.
Alfabetos mezclados sin avisar
base64.b64decode de Python descarta los caracteres fuera del alfabeto estándar salvo que pases validate=True. Si le das una cadena Base64URL, elimina los - y _ y decodifica el resto:
import base64
base64.b64decode("PDw_Pz8-Pg==")
# b'<<\x0f\xcf\xc3\xe0' (wrong; the input was "<<???>>")
base64.b64decode("PDw_Pz8-Pg==", validate=True)
# binascii.Error: Only base64 data is allowed
base64.urlsafe_b64decode("PDw_Pz8-Pg==")
# b'<<???>>'
La primera llamada devuelve bytes incorrectos sin lanzar ningún error. Usa urlsafe_b64decode para entradas URL-safe, o validate=True para que la discrepancia salte a la vista.
Doble codificación
Este nombre se usa para dos cosas distintas, y ambas ocurren en la práctica.
Codificación por porcentajes dos veces. Tu código escapa una cadena Base64 estándar una vez y un cliente HTTP la escapa otra, así que + pasa a %2B y luego a %252B. El servidor decodifica una vez y recibe un %2B literal. Base64URL evita el problema porque no hay nada que escapar.
Base64 de Base64. Un valor se codifica en dos capas, por ejemplo en la aplicación y otra vez en un framework o en las herramientas de manifiestos de Kubernetes. Man se convierte en TWFu y luego en VFdGdQ==. La pista es que al decodificar obtienes algo que sigue formado por caracteres Base64. Decodifica otra vez y comprueba si el resultado tiene sentido.
Expectativas sobre el relleno
Algunos receptores exigen relleno y otros lo rechazan. RawURLEncoding de Go falla con una entrada que contiene =, mientras que el decodificador de Python falla con una entrada que no lo tiene. Cuando definas una API, indica en la documentación la variante y la regla del relleno, y haz que tu propio decodificador acepte ambas formas.
Pega una cadena URL-safe, con o sin relleno, y mira qué contiene.
Preguntas frecuentes
¿Base64URL es lo mismo que aplicar codificación URL a una cadena Base64?
No. La codificación por porcentajes convierte + en %2B y / en %2F, lo que alarga la cadena. Base64URL usa otro alfabeto para que no haga falta escapar nada.
¿Un decodificador Base64 estándar puede leer Base64URL?
No de forma fiable. Algunos decodificadores, como Buffer de Node, aceptan ambos alfabetos. Otros rechazan - y _, y b64decode de Python los descarta sin avisar por defecto, lo que produce bytes incorrectos.
¿Base64URL lleva relleno?
RFC 4648 lo permite, y urlsafe_b64encode de Python lo conserva. La mayoría de los formatos que usan Base64URL, JWT incluido, omiten el = porque la longitud ya lo determina.
¿Cuál uso para tokens en URLs?
Base64URL sin relleno. No necesita escaparse en rutas, cadenas de consulta, cookies ni nombres de archivo.