O que pode e o que não pode derrubar uma geração
O generatePdf traça uma linha: problema no dado, ou em conteúdo mal
formado, degrada — o campo renderiza vazio, um caractere é trocado, o PDF sai.
Problema estrutural, ou em que conteúdo sumiria em silêncio de um documento
que alguém assina, falha alto. Um relatório de 200 páginas não pode morrer
porque uma linha tinha um \n.
Degrada (o PDF sai):
| Situação | O que acontece |
|---|---|
Caractere de controle no dado (\n, \t, NUL, C0/C1) | Vira espaço. Fonte nenhuma tem glifo pra esses, então é a única renderização possível |
Expressão {...} inválida | Aquele token renderiza vazio; o editor marca o campo |
| Path que não resolve | Vazio |
data null, array, string ou número | Ignorado; campos renderizam vazio |
| Array vinculado que é número, itens não-objeto, célula com valor objeto | Ignorado ou serializado |
Cor inválida ("banana", "#zzz") | Cai no default |
fontSize 0, negativo ou NaN | 0/negativo valem; NaN cai em 10 |
| Largura de campo 0 ou negativa | Vale (nada é desenhado) |
| Vínculo de imagem que resolve pra algo que não é data URI | Campo fica vazio |
| Repetição de seção mais alta que uma página inteira | É colocada mesmo assim, transbordando aquela página; a repetição seguinte começa numa nova |
Falha alto (e por quê):
Cada uma destas é uma classe tipada com um code literal, exportada dos
dois entries (json-pdf-designer e json-pdf-designer/server). A coluna
abaixo é o que você casa — nunca error.message, que é uma string de
diagnóstico em inglês e pode ser reescrita numa release de patch.
| Situação | Classe (code) | blame | Por que não degradar |
|---|---|---|---|
Caractere sem glifo na fonte padrão (emoji/CJK sem fontBytes) | UnsupportedGlyphError (unsupportedGlyph) | data | Ele tem glifo numa fonte completa — descartá-lo removeria conteúdo, em silêncio, de um documento que alguém assina. Passe fontBytes, ou tire o caractere do dado |
| Tamanho de página ausente, que não é objeto, ou que não são dois números finitos > 0 | InvalidPageSizeError (invalidPageSize) | template | Estrutural: não há default sensato pra adivinhar. Conferido em TODAS as páginas antes de renderizar qualquer uma, então uma página 7 torta não espera seis boas |
| Imagem corrompida num campo | ImageUnreadableError (imageUnreadable) | template | Quem montou escolheu aquele arquivo; descartar calado esconde o erro |
| Imagem corrompida como fundo da página | BackgroundImageUnreadableError (backgroundImageUnreadable) | template | Idem, e papel timbrado faltando não é óbvio no resultado |
| Formato de imagem que não é PNG nem JPEG | UnsupportedImageFormatError (unsupportedImageFormat) | template | Reenvie pelo editor, que converte |
| Uma imagem acima de 15MB (decodificada) | ImageTooLargeError (imageTooLarge) | template | Protege quem está gerando — template pode vir de fonte não confiável |
| Mais de 200 imagens distintas num documento | TooManyImagesError (tooManyImages) | template | Idem. Imagens idênticas são deduplicadas por conteúdo antes, então isso conta as de verdade |
| Upload acima de 20MB, ou que o navegador não consegue ler | ImageUploadTooLargeError / ImageUploadUnreadableError | data | Pego no upload, antes de poder chegar num template |
Documento acima de maxPages (default DEFAULT_MAX_PAGES, 5000) | PageLimitError (pageLimit) | data | Relatório truncado que parece completo é pior que relatório nenhum. Filtre o dado, divida em vários PDFs, ou aumente o maxPages |
| Uma passada de paginação que não consome nada | PaginationStalledError (paginationStalled) | package | Bug de aritmética do pacote — girar até um contador estourar escondia isso |
.woff2 passado onde o runtime não consegue descomprimir | Woff2SupportMissingError / FontDecompressFailedError / FontDecompressTimeoutError | config | Problema de ambiente, não de template |
Template que não é objeto, ou version que não é número | TemplateNotAnObjectError / TemplateVersionInvalidError | template | Não há de onde migrar |
Template.version mais novo que este build | TemplateVersionTooNewError (templateVersionTooNew) | template | Formato mais novo pode carregar campo que este build ignoraria calado |
| Falta um passo de migração entre duas versões | TemplateMigrationMissingError (templateMigrationMissing) | package | Lacuna nossa, não sua |
fontBytes inválido (Unknown font format) | não é nosso — quem lança é o pdf-lib | — | Erro de quem chamou, não do template nem do dado. Veja o caso null abaixo |
Como tratar
Três linhas, e a ordem importa:
import { describePdfError, dictFor, PageLimitError } from "json-pdf-designer/server";
try {
const bytes = await generatePdf(template, data, bindings, { fontBytes });
} catch (err) {
// 1. Um caso específico que você quer tratar à parte — por classe, não por mensagem.
if (err instanceof PageLimitError) return dividirETentarDeNovo(err.maxPages);
// 2. Todo o resto: uma chamada transforma o erro em texto que uma pessoa lê.
const problem = describePdfError(err, dictFor("pt-BR"));
// → { code, blame, title, action?, field?, detail } | null
}
O describePdfError é exaustivo, então não existe ramo "não reconheci este"
dentro dele. Ele devolve null pra erro que não é do pacote — a linha do
Unknown font format acima é exatamente esse caso — em vez de inventar um
título pra uma falha que ele não entende. Guarde a sua cópia pros seus erros, e
caia num genérico só quando as duas coisas vierem vazias.
Se você quer o estreitamento sem a cópia — log, métrica, um status HTTP e nada
mostrado pra ninguém — o isPdfError(err) estreita unknown pra
AnyPdfError, o que já te dá code e blame sem dicionário nenhum
envolvido.
title e action saem no idioma do dict que você passou. Guarde os dois, e
eles congelam naquele idioma — então quem troca o idioma da interface vê aquele
pedaço da tela ficar atrás, sem nada falhar e sem nada no log. Guarde o erro
cru e descreva na renderização.
- catch (err) { setErro(describePdfError(err, dictFor(locale))); }
+ catch (err) { setCaixaDeErro({ err }); }
+ // …e no corpo do render, pra rederivar a cada troca de idioma:
+ const problem = caixaDeErro && describePdfError(caixaDeErro.err, dictFor(locale));
A caixa ({ err }) não é enfeite: null não distingue "sem erro" de "um erro
que é null", e setErro(err) com um erro que por acaso seja função seria lido
pelo React como updater.
Num servidor
O blame existe pra esta decisão não precisar ser tomada duas vezes:
blame | O que significa | Status razoável |
|---|---|---|
data | O payload não consegue produzir este documento | 400, ou 413 no caso do pageLimit |
template | O template salvo está errado | 422 |
config | Falta algo no ambiente (uma fonte, um codec) | 500, e acorde alguém |
package | Bug nosso | 500, e por favor abra um issue |
Nada acima precisa de React: as classes, o isPdfError, o describePdfError e
o dictFor saem todos do json-pdf-designer/server.
Uma ressalva sobre o erro de glifo. Ele só dispara com a fonte padrão
(Helvetica/WinAnsi). Com uma fonte embutida via fontBytes, um caractere que a
fonte não cobre é desenhado com o glifo .notdef dela — normalmente um branco
ou um quadrado — sem erro nenhum. Isso é comportamento do fontkit, não uma
escolha deste pacote, e significa que uma fonte customizada troca uma falha
alta por um branco silencioso. Se glifo ausente não pode passar despercebido,
confira o dado antes de gerar.