Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

String Decoder no Node.js: Guia Prático

Atualizado em: 10 de agosto de 2026

Terminal Linux usado em desenvolvimento e administração de aplicações Node.js

Streams entregam dados em blocos que não respeitam necessariamente os limites dos caracteres. Em UTF-8, uma letra acentuada ou emoji pode ocupar vários bytes e ser dividida entre dois chunks. Converter cada bloco isoladamente com toString() pode produzir o caractere de substituição e corromper o texto. O String Decoder no Node.js resolve esse problema mantendo bytes incompletos até o próximo bloco.

O módulo é pequeno, mas importante em protocolos, sockets, arquivos e parsers que trabalham diretamente com Buffers. Em muitos casos, configurar a codificação da própria stream já usa um decoder internamente. Ainda assim, conhecer a API ajuda a criar transformações personalizadas e diagnosticar textos quebrados.

Neste guia, você aprenderá a usar StringDecoder, write(), end(), diferentes codificações, integração com streams, limites de memória e testes de caracteres multibyte.

O problema dos caracteres divididos

UTF-8 usa de um a quatro bytes por ponto de código. Um chunk pode terminar no meio da sequência:

const text = Buffer.from('ação', 'utf8');
const first = text.subarray(0, 3);
const second = text.subarray(3);

console.log(first.toString('utf8'));
console.log(second.toString('utf8'));

Dependendo do corte, a saída pode conter . Os bytes não estão errados; a conversão ocorreu antes de todos chegarem.

A documentação oficial de String Decoder descreve a API. Para fundamentos binários, veja Buffer no Node.js e Streams no Node.js. A referência do Unicode Standard explica codificação de caracteres.

Criando um StringDecoder

const { StringDecoder } = require('node:string_decoder');

const decoder = new StringDecoder('utf8');

A codificação padrão é UTF-8. Também existem opções compatíveis como utf16le, latin1, base64 e outras reconhecidas pelo Buffer, conforme a versão.

Usando write()

const decoder = new StringDecoder('utf8');
const buffer = Buffer.from('Código Fácil');

const first = buffer.subarray(0, 8);
const second = buffer.subarray(8);

console.log(decoder.write(first));
console.log(decoder.write(second));

Se o primeiro bloco terminar com bytes incompletos, o decoder os retém. Eles serão combinados com o próximo write().

Finalizando com end()

const finalText = decoder.end();

end() informa que não haverá mais dados. Caso existam bytes incompletos, a saída poderá conter um caractere de substituição, pois a sequência nunca foi concluída.

Também é possível enviar o último Buffer:

const finalText = decoder.end(lastChunk);

Exemplo completo com chunks

const { StringDecoder } = require('node:string_decoder');

function decodeChunks(chunks) {
  const decoder = new StringDecoder('utf8');
  let result = '';

  for (const chunk of chunks) {
    result += decoder.write(chunk);
  }

  result += decoder.end();
  return result;
}

Esse código é adequado para dados pequenos. Acumular uma resposta ilimitada em string pode esgotar memória. Em arquivos grandes, processe o texto progressivamente.

Streams com setEncoding()

Uma Readable Stream pode decodificar automaticamente:

stream.setEncoding('utf8');

stream.on('data', chunk => {
  console.log(typeof chunk); // string
});

O Node.js utiliza lógica de decodificação que preserva caracteres multibyte entre chunks. Para a maioria dos consumidores de texto, setEncoding() é mais simples que instanciar StringDecoder manualmente.

Quando usar StringDecoder diretamente?

  • ao implementar um protocolo binário com partes textuais;
  • ao controlar manualmente os Buffers recebidos;
  • ao criar um parser que alterna entre texto e dados binários;
  • ao integrar uma API que entrega chunks fora de uma stream padrão;
  • ao testar explicitamente limites de caracteres.

Lendo arquivo como texto

Se o arquivo cabe em memória, fs.readFile() com encoding já retorna string:

const fs = require('node:fs/promises');

const content = await fs.readFile('data.txt', 'utf8');

Para arquivo grande:

const stream = createReadStream('data.txt', {
  encoding: 'utf8'
});

Nesses casos, não é necessário criar decoder manual.

Decodificando sockets TCP

const net = require('node:net');
const { StringDecoder } = require('node:string_decoder');

const server = net.createServer(socket => {
  const decoder = new StringDecoder('utf8');

  socket.on('data', chunk => {
    const text = decoder.write(chunk);
    processText(text);
  });

  socket.on('end', () => {
    const remainder = decoder.end();
    if (remainder) processText(remainder);
  });
});

Decodificação correta não resolve enquadramento de mensagens. Uma mensagem JSON pode ser dividida em vários chunks ou várias mensagens podem chegar no mesmo chunk. O protocolo precisa de delimitador, tamanho ou outra estratégia.

Parser por linhas

const decoder = new StringDecoder('utf8');
let pending = '';

function receive(chunk) {
  pending += decoder.write(chunk);

  let newline;
  while ((newline = pending.indexOf('\n')) !== -1) {
    const line = pending.slice(0, newline);
    pending = pending.slice(newline + 1);
    processLine(line);
  }
}

No encerramento:

pending += decoder.end();
if (pending) processLine(pending);

Aplique limite ao tamanho de pending. Um cliente que nunca envia quebra de linha pode fazer a string crescer indefinidamente.

Limite de mensagem

const MAX_LINE_LENGTH = 64 * 1024;

if (Buffer.byteLength(pending, 'utf8') > MAX_LINE_LENGTH) {
  throw new Error('Linha excedeu o limite');
}

Contar caracteres e contar bytes são operações diferentes. Para protocolos com limite em bytes, controle o tamanho antes ou durante a decodificação.

UTF-16LE

const decoder = new StringDecoder('utf16le');

UTF-16 pode dividir unidades de código e pares substitutos. O decoder mantém as partes necessárias. Confirme se a origem realmente utiliza little-endian e se existe BOM.

Base64

O decoder também pode produzir Base64 a partir de bytes, preservando grupos incompletos:

const decoder = new StringDecoder('base64');

let encoded = decoder.write(chunk1);
encoded += decoder.write(chunk2);
encoded += decoder.end();

Base64 é codificação, não criptografia. Não use para proteger segredos.

Latin1 e ASCII

Em codificações de um byte, não existe o mesmo problema de sequência multibyte, mas a interpretação pode perder caracteres. Latin1 mapeia cada byte para um caractere. ASCII restringe valores e pode truncar bits conforme a implementação.

Não tente corrigir texto UTF-8 corrompido escolhendo Latin1 sem conhecer a origem. Isso apenas muda a forma do erro.

Detecção de codificação

StringDecoder não detecta automaticamente a codificação. O protocolo, header, formato do arquivo ou configuração precisa informar o charset. Bibliotecas de detecção trabalham por heurística e podem errar.

TextDecoder como alternativa

A API padrão TextDecoder também suporta decodificação incremental:

const decoder = new TextDecoder('utf-8');

let text = decoder.decode(chunk1, { stream: true });
text += decoder.decode(chunk2, { stream: true });
text += decoder.decode();

TextDecoder é alinhado às APIs Web e pode ser conveniente em código compartilhado entre navegador e Node.js. StringDecoder continua integrado ao ecossistema histórico de Buffers.

Modo fatal

TextDecoder pode oferecer opções como fatal, rejeitando sequências inválidas em vez de substituir. StringDecoder normalmente substitui dados incompletos ou inválidos. Escolha de acordo com o contrato.

Validação de UTF-8

Se dados inválidos devem causar erro, valide explicitamente antes ou use uma API com modo fatal. Para logs e interfaces tolerantes, substituição pode ser aceitável. Para assinatura, identificadores ou protocolos rígidos, aceitar texto corrompido pode ser perigoso.

Decodificação e segurança

Caracteres Unicode semelhantes visualmente podem ser diferentes. Normalização, comparação de identificadores e regras de nomes precisam considerar homógrafos e formas compostas.

const normalized = text.normalize('NFC');

Não normalize dados binários, hashes, assinaturas ou campos cujo protocolo exige bytes exatos.

Tratando BOM

Alguns arquivos começam com Byte Order Mark. O decoder pode retornar o caractere BOM no texto. Remova apenas quando o formato permitir:

function stripBom(text) {
  return text.charCodeAt(0) === 0xFEFF
    ? text.slice(1)
    : text;
}

Testando cortes em todos os bytes

Um teste robusto divide a string em todas as posições:

const original = 'Olá 👋 ação';
const bytes = Buffer.from(original, 'utf8');

for (let index = 0; index <= bytes.length; index += 1) {
  const decoder = new StringDecoder('utf8');
  const result =
    decoder.write(bytes.subarray(0, index)) +
    decoder.end(bytes.subarray(index));

  assert.equal(result, original);
}

Esse teste confirma que qualquer divisão de chunk mantém o texto.

Testando dados incompletos

const decoder = new StringDecoder('utf8');
const incomplete = Buffer.from([0xF0, 0x9F]);

const result = decoder.end(incomplete);
assert.match(result, /�/);

Não dependa da quantidade exata de caracteres de substituição sem verificar o contrato da versão.

Observabilidade

Conte ocorrências de dados inválidos ou linhas excedidas. Não registre o payload completo, pois pode conter informações pessoais ou bytes maliciosos.

Erros comuns

  • Chamar toString em cada chunk: caracteres multibyte são corrompidos.
  • Esquecer end(): bytes finais permanecem pendentes.
  • Acumular texto sem limite: memória cresce com a entrada.
  • Confundir chunk com mensagem: framing do protocolo fica incorreto.
  • Adivinhar encoding: texto é interpretado de forma errada.
  • Usar Base64 como segurança: conteúdo é facilmente reversível.
  • Ignorar Unicode: comparações e identificadores ficam inconsistentes.

Boas práticas para produção

  • Defina a codificação no protocolo.
  • Use setEncoding para streams textuais simples.
  • Use StringDecoder em parsers manuais.
  • Sempre finalize com end.
  • Separe decodificação de framing.
  • Limite buffers e mensagens pendentes.
  • Valide dados quando substituição não for aceitável.
  • Teste cortes em caracteres multibyte.
  • Normalize Unicode apenas quando a regra permitir.
  • Evite payload sensível em logs.

Conclusão

O String Decoder no Node.js impede que caracteres multibyte sejam corrompidos quando seus bytes chegam em chunks separados. Ele mantém fragmentos incompletos e libera texto somente quando a sequência pode ser interpretada.

Decodificação é apenas uma camada. Protocolos ainda precisam de framing, limites e validação de charset. Ao combinar StringDecoder com controle de tamanho e testes de cortes, aplicações conseguem processar texto de sockets e streams de forma correta e previsível.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita