Obrigado Open Source

Quando não usar response.text() do fetch.

O cenário era o seguinte, quando um hook é chamado, o meu back-end precisa recuperar um PDF de um serviço externo que fica disponível por poucos minutos para enviar pro S3. Essa integração já existente por alguns meses em outros serviços sempre funcionou sem problemas, porém o serviço era péssimo p/ gerir os documentos e precisamos trocar para um serviço que tinha um painel melhor de gestão para operação do negócio. E aqui começa nossos problemas.

No início achamos que era só manter a integração com uma pequena mudança. A integração original era basicamente a seguinte.

// obtem a url do documento
const { name, url } = await signatureService.downloadDocument(uuid);
// obtem o documento
const requestResult = await fetch(url);
// obtem a resposta em texto da chamada
const pdf = await requestResult.text();
// converte em base64
const b64File = Buffer.from(pdf).toString('base64');
// Salva o documento no S3
const { base64, ...s3Result } = await s3Service.upload(
        'doc.pdf',
        b64File,
        buckets.documents
      );

Essa integração funcionou por meses e bastava trocar os serviços e usarmos algo próximo da seguinte implementação, mudando só pequenas partes como o uso da URL assinada do S3 do parceiro.

Só que a coisa não foi bem por aí, foi quando começamos a ter os problemas com os documentos serem salvos com a 1º página em branco, enquanto o restante era exibido, porém sem as imagens anexada no documento original.

Começando as hipóteses

Primeiro foi a questão de como o texto estava sendo tratado e o encoding, apesar de termo outras hipóteses esse se mostrou o problema correto, mas a solução inicial foi problemática.

Segunda hipótese foi o encoding do windows estar atrapalhando essa geração dos documentos. A primeira integração foi desenvolvida e sempre rodou em ambiente de base unix essa segunda integração estava sendo desenvolvida no windows. Aqui vamos para 1ª dica quando se deparamos com esses problemas, normalização do ambiente.

Testamos as implementações em computador que tivesse base unix, para ver se o problema se mantia e continuou acontecendo. Hora de atacar o primeiro problema.

ISO-8851

Em tudo que fazemos usamos utf-8, por ser uma encoding com suporte de larga escala e bem comum no NodeJS.

Como nossa 1ª hipótese era problema no encoding, trocamos a conversação para ISO-8851, um formato também muito popular e antigo de lidar com documentos, porém como o outro serviço era fora do nosso controle, parece uma boa tentar esse formato. Agora o encoding quebrava diferente do anterior, mas continuava quebrando.

Busca mais complicada, para solução relativamente simples

Quando o nível de strings não parece ter solução, precisamos descer para o nível dos bytes e foi aqui que comecei a olhar para soluções usando a Readable Stream do fetch e que no Node usa o formato de Web Readable Stream ao contrário da API nativa de Streams do Node. A biblioteca que undici que usamos fornece esse suporte.

Porém sem sucesso em gerar o base 64 correto. Foi quando decidi ir a fundo e notei que o PDF salvo na minha máquina tinha a assinatura da lib que gerou PDF no serviço parceiro.

Aqui eu estava empenhado em encontrar qual era o encoding que gerava esse PDF e fazer a conversão da string se necessário (Não foi necessário). Porém essa pesquisa me levou no caminho correto.

Na pesquisa cai no gitlab da biblioteca (obrigado Open Source), na questão encontrei vi a forma que o encoding fica marcado dentro o PDF. Agora era só uma questão de fazer essa busca e formatar o Buffer.from com o encoding correto. (eu ignorei que PDF é um binário).

const text = await response.text();
const re = /(\/Encoding .*)/i; // match para encontrar o encoding do PDF
const found = text.match(re);
console.log(found);

Com isso descobri o formato winansiencoding, agora é só pesquisar com o converter isso para utf-8 e estaria com os problemas resolvidos. Já tinha até preparado o TextDecoder.

Quando cai nessa pergunta do Stackoverflow

Displaying UTF-8 characters in PDF
I am trying to display a PDF by converting it into a binary string from the backend.This is the ajax call I am making $.ajax({ type : ‘GET’, url : ‘<url>’, ...

Apesar do cenário ser relacionado ao front, percebi um detalhe quando na resposta fala sobre PDF serem dados puros e veio a lembrança de que PDFs são binários e não fazia sentido nenhum tratar como texto e que foi uma coincidência o serviço anterior ter funcionado.

Response.body

A response do fetch tem a opção .bodyonde podemos acessar o Readable Stream da resposta e lidar com a stream da chamada. Aqui cabe um lembrete de que tudo no Node é baseado nisso e falei sobre isso nesse texto de 2015.

Obtendo as respostas
Após vermos o console.log do NodeJS vamos começar com o módulo http e trabalhar com requests e responses. Uma das coisas mais legais do Node é a sua modularidade. O módulo http é o responsável pelas…

A diferença é que agora temos APIs melhores e modernas para tratar isso.

Porém Web Readable Streams, geram chunks separados para o mesmo arquivo e isso precisa ser iterado até formar uma uníca coisa. E com isso vamos para o código abaixo.

const chunks = [];
for await (const chunk of response.body.values({ preventCancel: true })) 
  chunks.push(chunk);

Isso por si só não resolve, já que vamos precisar criar um Uint8Array, para o Buffer transformar em base64 e estamos usando o array normal do JS.

Um Uint8Array precisa ter o tamanho definido assim que instanciado, então junto do nosso for await também criamos esse acumulador que chamamos de totalChunk.

Com isso podemos ir para a próxima etapa que é transformar o nosso array comum no novo Uint8Array. Que fazemos com o seguinte código.

const pdfText = new Uint8Array(totalChunk);

chunks.forEach(chunk => {
  pdfText.set(chunk, offset);
  offset += chunk.length;
});

Pronto agora temos um novo array que o Buffer do Node entende bem e basta só usar a seguinte linha

const b64File = Buffer.from(pdfText).toString('base64');

Com isso o PDF não precisa de nenhuma transformação do encoding e é salvo sem alterações no seu conteúdo.

As ideias de tentar converter algum tipo de encoding caem totalmente aqui. Dados binários são dados binários. Espero que esse texto ajude alguém no futuro.