Загрузить файл
  1. base64hub
  2. Base64 в коде
  3. Node.js
Код

Base64 в Node.js

Node.js работает с Base64 через Buffer, где base64 и base64url – встроенные кодировки. Все результаты ниже получены на Node.js 24.

Закодировать и декодировать строку

Buffer.from(string, "utf8") превращает текст в байты UTF-8, а .toString("base64") их кодирует. Декодирование – те же два вызова в обратном порядке.

const enc = Buffer.from("héllo wörld", "utf8").toString("base64");
console.log(enc);                                         // aMOpbGxvIHfDtnJsZA==

console.log(Buffer.from(enc, "base64").toString("utf8")); // héllo wörld

"utf8" – значение по умолчанию в обоих вызовах, но если написать его явно, намерение сразу видно. Buffer – подкласс Uint8Array, так что обычный Uint8Array нужно обернуть: u8.toString("base64") игнорирует аргумент и возвращает "104,105", а Buffer.from(u8).toString("base64") возвращает aGk=.

URL-safe Base64: кодировка base64url

Для URL-safe алфавита в Node есть отдельная кодировка. Дополнение она никогда не пишет:

Buffer.from("<<???>>").toString("base64");     // PDw/Pz8+Pg==
Buffer.from("<<???>>").toString("base64url");  // PDw_Pz8-Pg
Buffer.from("PDw_Pz8-Pg", "base64url").toString();  // <<???>>

Если получателю нужен URL-safe Base64 с дополнением, допишите его сами: s + "=".repeat((4 - s.length % 4) % 4).

При декодировании Node снисходителен: и "base64", и "base64url" принимают любой из двух алфавитов и не обращают внимания на дополнение.

Buffer.from("PDw_Pz8-Pg==", "base64").toString();   // <<???>>
Buffer.from("PDw/Pz8+Pg==", "base64url").toString(); // <<???>>

Для JWT и токенов это удобно (попробуйте декодер JWT), но прочитайте раздел о проверке ниже. Подробно о двух алфавитах рассказано в статье Base64 и Base64URL.

Закодировать и декодировать файл

fs принимает "base64" в качестве кодировки, и это экономит шаг в обе стороны:

import { readFileSync, writeFileSync } from "node:fs";

readFileSync("hello.txt").toString("base64");  // aGVsbG8K
readFileSync("hello.txt", "base64");           // aGVsbG8K  (same thing)

writeFileSync("copy.txt", Buffer.from("aGVsbG8K", "base64"));
writeFileSync("copy2.txt", "aGVsbG8K", "base64");  // decodes while writing

Чтобы встроить файл в HTML или CSS, добавьте к результату префикс: `data:image/png;base64,${b64}`. Генератор data URI собирает такую же строку в браузере.

Большие файлы потоком

Читать файл на 2 ГБ в память ради кодирования – плохая идея. Поток Transform может кодировать по частям, если кодирует только целые группы по 3 байта, а остаток переносит в следующую часть:

import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Transform } from "node:stream";

class Base64Encode extends Transform {
  constructor() { super(); this.rest = Buffer.alloc(0); }
  _transform(chunk, _enc, cb) {
    const buf = Buffer.concat([this.rest, chunk]);
    const cut = buf.length - (buf.length % 3);
    this.rest = buf.subarray(cut);
    cb(null, buf.subarray(0, cut).toString("base64"));
  }
  _flush(cb) { cb(null, this.rest.toString("base64")); }
}

await pipeline(createReadStream("big.bin"), new Base64Encode(),
               createWriteStream("big.b64"));

На случайном файле в 1 МБ результат в точности совпал с readFileSync("big.bin").toString("base64"). Без переноса остатка каждая граница частей, не кратная 3, вставляла бы дополнение = в середину вывода. Почему так, объяснено в статье как работает Base64.

Чтобы узнать размер декодированных данных, не декодируя их, вызовите Buffer.byteLength("aGVsbG8K", "base64") – он вернёт 6.

Проверка входных данных

Buffer.from(s, "base64") никогда не бросает исключение из-за плохого содержимого. Он отбрасывает символы, которых нет ни в одном алфавите, и возвращает то, что осталось:

Buffer.from("aGk=!!", "base64");             // <Buffer 68 69>
Buffer.from("not base64 at all", "base64");  // <Buffer 9e 8b 5b 6a c7 ba e1 ab 5a 96>
Buffer.from("aGk", "base64").toString();     // hi  (missing padding is fine)

Для недоверенных данных проверьте алфавит и длину, затем закодируйте результат заново и сравните. Такая проверка туда и обратно ловит ненулевые биты дополнения, например aGl=: она тоже декодируется в hi, но каноническим Base64 не считается.

function isBase64(s) {
  return /^[A-Za-z0-9+/]*={0,2}$/.test(s) && s.length % 4 === 0 &&
         Buffer.from(s, "base64").toString("base64") === s;
}

isBase64("aGk=");        // true
isBase64("aGk");         // false (no padding)
isBase64("aGl=");        // false (non-canonical)
isBase64("not base64");  // false

Для base64url замените регулярное выражение на [A-Za-z0-9_-] и уберите проверку длины. Валидатор Base64 делает те же проверки интерактивно.

Подводные камни и сообщения об ошибках

  • Аргумент не того типа. Buffer.from(123, "base64") бросает TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type number (123). Обычно это undefined или число, проскочившее из JSON.
  • new Buffer(). Конструктор устарел (DEP0005) из соображений безопасности. Используйте Buffer.from или Buffer.alloc.
  • Декодирование двоичных данных как текста. .toString("utf8") на байтах картинки без всякой ошибки выдаёт символы замены (U+FFFD), и записанный обратно файл оказывается испорчен. Храните двоичные данные в Buffer.
  • Префикс data URI. Buffer.from("data:image/png;base64,iVBOR...", "base64") не упадёт, а декодирует префикс как мусор. Сначала разделите строку по запятой.
  • Переносы строк. При декодировании Node игнорирует \n и \r, так что строки с переносами в стиле MIME работают, но кодировщик сам строки не переносит. Если нужны строки MIME, используйте b64.match(/.{1,76}/g).join("\r\n").

Версии

  • Кодировка base64url: Node.js 15.7.0 и 14.18.0.
  • Глобальные btoa и atob: Node.js 16.0, помечены как устаревшие.
  • Uint8Array.prototype.toBase64 и Uint8Array.fromBase64 в Node.js 24 недоступны (typeof возвращает "undefined"). Они придут с более новыми версиями V8, а пока остаётся Buffer.
  • Buffer.from(string, "base64") во всех версиях игнорировал пробелы и недопустимые символы, так что для проверки на него не полагайтесь.

Вопросы и ответы

Бросает ли Buffer.from(str, 'base64') исключение на некорректных данных?

Нет. Node пропускает символы вне алфавита и останавливается на первом =, так что мусор без всякой ошибки декодируется в какие-то байты. Если входным данным нельзя доверять, проверяйте строку сами.

В какой версии Node.js появился base64url?

Кодировка base64url появилась в Node.js 15.7.0 и была перенесена в 14.18.0. В любой актуальной версии на неё можно рассчитывать.

Стоит ли использовать btoa в Node.js?

Node помечает btoa и atob как устаревшие и рекомендует Buffer. Они существуют ради совместимости с браузером и страдают тем же ограничением Latin-1, что описано на странице Base64 в JavaScript.

Читайте также