Por que atob() retorna bytes e btoa() rejeita algum texto

Entenda as strings binárias JavaScript, os erros de intervalo de caracteres e a etapa UTF-8 necessária para atob e btoa.

Os nomes atob e btoa pode sugerir conversão de texto comum. O contrato do navegador é mais restrito: eles convertem entre texto Base64 e uma string cujos valores de caracteres representam bytes individuais. Essa sequência de bytes é um contêiner, não uma promessa de que o resultado é um texto Unicode legível.

Duas cordas diferentes

As strings JavaScript usam unidades de código UTF-16. Uma sequência de bytes restringe deliberadamente cada valor de caractere de 0 a 255. btoa('Hello') funciona porque os valores ASCII se enquadram nesse intervalo. btoa('你好') falha porque esses caracteres não cabem em um byte. Mesmo um personagem que se encaixe pode ser enganoso: btoa('é') codifica o byte único E9, não os UTF-8 bytes C3 A9 esperados por muitos serviços.

console.log(btoa('é')); // 6Q== (byte E9)
console.log(btoa(String.fromCharCode(0xc3, 0xa9))); // w6k= (UTF-8)

Coloque a conversão UTF-8 em torno das funções de byte

TextEncoder produz UTF-8 bytes de texto. Após a decodificação de Base64, um erro fatal TextDecoder converte bytes UTF-8 válidos de volta em texto e rejeita sequências inválidas em vez de substituí-las silenciosamente. Este pequeno exemplo é deliberadamente explícito para que o limite fique visível.

const text = '中文😊';
const bytes = new TextEncoder().encode(text);
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
const encoded = btoa(binary);
console.log(encoded); // 5Lit5paH8J+Yig==
const decodedBytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(decodedBytes);
console.log(decoded); // 中文😊

Não trate um resultado binário como uma string quebrada

atob('/w==') representa o byte FF. Não é UTF-8 válido por si só, mas são dados binários perfeitamente válidos. A próxima ação correta pode ser um download de bytes ou visualização hexadecimal, e não um truque de conversão de texto diferente. Executando essa string de bytes TextEncoder codificaria o caractere U+00FF e alteraria os bytes.

O decodificador do navegador também não é um validador de protocolo completo. Os espaços em branco aceitos e o preenchimento omitido podem diferir dos requisitos rígidos do aplicativo. Se um protocolo exigir codificação canônica, valide o alfabeto, o preenchimento e os bits não utilizados separadamente. Para arquivos grandes, evite espalhar uma matriz de bytes completa em String.fromCharCode(...bytes); os limites de contagem de argumentos podem falhar muito antes de o arquivo se tornar excepcionalmente grande.

Leia os contratos do navegador em MDN: atob() e MDN: btoa(). O aplicativo deve decidir se deseja uma conversão permissiva, um validador estrito ou um fluxo de trabalho de preservação de arquivos, em vez de assumir que são a mesma tarefa.