Upload de arquivos parece um detalhe simples de produto: receber um PDF, uma imagem, um CSV ou um pacote gerado pelo usuário. Em produção, porém, ele vira uma das fronteiras mais perigosas de uma API. O cliente pode enviar mais bytes do que o esperado, mentir sobre o tipo do arquivo, interromper a conexão no meio, repetir requisições, tentar path traversal no nome original ou forçar o servidor a carregar tudo em memória.
Zig é uma boa linguagem para essa classe de problema porque deixa I/O, alocação, buffers e erros explícitos. O mesmo controle que ajuda em servidores HTTP em produção, parsers de arquivos grandes e processamento de dados em streaming também ajuda a construir um fluxo de upload previsível. A desvantagem é que você não deve esperar um framework esconder todos os detalhes. Em Zig, o desenho seguro precisa aparecer no código.
Este guia mostra como pensar em upload de arquivos em Zig para APIs backend: limites de tamanho, streaming, validação, checksum, armazenamento, observabilidade e respostas HTTP. A ideia não é vender uma biblioteca específica, mas montar um checklist técnico que funciona com std.http.Server, com um roteador pequeno ou com uma camada própria atrás de Nginx, Caddy, Traefik ou Cloudflare.
O erro clássico: ler tudo em memória
O primeiro impulso de muitos handlers é ler o corpo inteiro da requisição, colocar em um []u8 e só então validar. Isso funciona no exemplo pequeno, mas falha rápido em produção. Um endpoint que deveria aceitar arquivos de 5 MB pode receber 500 MB por erro de cliente, ataque, crawler ruim ou proxy mal configurado. Se cada conexão aloca um buffer gigante, poucos uploads derrubam o processo.
O fluxo mais seguro é tratar o upload como stream:
- validar headers e autenticação antes de ler o corpo;
- impor um limite máximo no proxy e no processo Zig;
- ler em chunks de tamanho fixo;
- atualizar checksum incrementalmente;
- escrever em arquivo temporário ou storage intermediário;
- abortar assim que o limite, tipo ou formato falhar;
- promover o arquivo para o destino final só depois da validação.
Essa divisão evita que a aplicação dependa de memória proporcional ao tamanho total do arquivo. O processo ainda precisa de buffers, mas eles ficam previsíveis.
Coloque limite na borda e no Zig
O reverse proxy deve bloquear uploads grandes antes que cheguem ao binário. No Nginx, por exemplo:
server {
listen 443 ssl http2;
server_name api.exemplo.com;
client_max_body_size 20m;
location /upload {
proxy_pass http://127.0.0.1:8080;
proxy_request_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
client_max_body_size é a primeira barreira. proxy_request_buffering off pode ser útil quando você quer streaming real até o backend, mas muda o comportamento operacional: o processo Zig passa a lidar com clientes lentos por mais tempo. Se o proxy bufferiza a requisição, ele protege o backend de parte do problema, mas adiciona uso de disco/memória no proxy. A escolha depende do ambiente.
Mesmo com proxy, o Zig também precisa conferir o limite. Ambientes de teste, chamadas internas e mudanças de infraestrutura podem contornar a borda. Trate o limite como regra da aplicação, não só configuração externa.
const std = @import("std");
const max_upload_bytes: u64 = 20 * 1024 * 1024;
fn copyLimited(reader: anytype, writer: anytype) !u64 {
var buffer: [16 * 1024]u8 = undefined;
var total: u64 = 0;
while (true) {
const n = try reader.read(&buffer);
if (n == 0) break;
total += n;
if (total > max_upload_bytes) return error.UploadTooLarge;
try writer.writeAll(buffer[0..n]);
}
return total;
}
O exemplo é intencionalmente pequeno. Em um handler real, reader viria do corpo HTTP e writer poderia apontar para um arquivo temporário. O ponto importante é que o limite é conferido durante a leitura, não depois.
Não confie no nome original
O nome enviado pelo cliente é metadado de exibição, não caminho de armazenamento. Nunca use diretamente algo como ../../etc/passwd, relatorio final.pdf, foto.png.exe ou nomes com caracteres invisíveis para compor o caminho final no servidor.
Um padrão mais seguro:
- gere um ID próprio para o arquivo;
- armazene em diretório controlado;
- preserve a extensão apenas se ela for validada;
- guarde o nome original em banco, escapado, para exibição;
- nunca permita que o usuário escolha subdiretório;
- use permissões restritas no diretório de upload.
Em vez de salvar uploads/<nome-original>, prefira algo como:
/var/app/uploads/tmp/01JZ...upload
/var/app/uploads/final/2026/06/03/01JZ...pdf
O arquivo temporário existe enquanto a validação está em andamento. Só depois de concluir tamanho, checksum e tipo, ele é movido para o destino final. Se o processo cair no meio, uma rotina de limpeza pode apagar temporários antigos.
Valide tipo pelo conteúdo, não só pelo header
Content-Type: image/png é uma declaração do cliente. Ela ajuda na UX, mas não prova que o arquivo é PNG. Para uploads sensíveis, leia os primeiros bytes e confira assinatura básica do formato.
fn looksLikePng(bytes: []const u8) bool {
const sig = [_]u8{ 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a };
return bytes.len >= sig.len and std.mem.eql(u8, bytes[0..sig.len], sig[0..]);
}
fn looksLikePdf(bytes: []const u8) bool {
return bytes.len >= 5 and std.mem.eql(u8, bytes[0..5], "%PDF-");
}
Isso não substitui validação completa de formato, antivírus ou análise assíncrona quando o risco é alto. Mas já evita o erro básico de aceitar qualquer conteúdo apenas porque o header dizia application/pdf.
Para CSV, JSON ou logs, a validação costuma ser semântica: limite de linhas, tamanho máximo de campo, encoding aceito, delimitador esperado e regras de negócio. Para isso, combine upload em streaming com os guias de JSON em streaming e leitura linha por linha.
Calcule checksum durante a cópia
Checksum ajuda em deduplicação, auditoria, cache, integridade e investigação de falhas. Não leia o arquivo duas vezes se você pode atualizar o hash enquanto grava.
const std = @import("std");
fn copyWithSha256(reader: anytype, writer: anytype) ![32]u8 {
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
var buffer: [16 * 1024]u8 = undefined;
var total: u64 = 0;
while (true) {
const n = try reader.read(&buffer);
if (n == 0) break;
total += n;
if (total > max_upload_bytes) return error.UploadTooLarge;
hasher.update(buffer[0..n]);
try writer.writeAll(buffer[0..n]);
}
var digest: [32]u8 = undefined;
hasher.final(&digest);
return digest;
}
O checksum não torna o arquivo seguro. Ele apenas prova que aqueles bytes específicos foram recebidos e armazenados. Segurança vem do conjunto: autenticação, autorização, limite, validação, isolamento, varredura quando necessária e controle de acesso na hora do download.
Multipart exige parser cuidadoso
Muitos uploads chegam como multipart/form-data. Esse formato permite campos de texto e arquivos na mesma requisição, mas também traz detalhes chatos: boundary, headers por parte, nomes de campo, múltiplos arquivos, campos repetidos e partes incompletas.
Se você implementar parser próprio, trate como código de infraestrutura crítica. Use limites por parte, limite total, limite de quantidade de campos, tamanho máximo do nome, tamanho máximo de header e erro claro para boundary inválido. Um parser ingênuo pode aceitar payload ambíguo ou manter bytes demais em memória.
Para muitos produtos, uma alternativa mais simples é evitar multipart no endpoint principal:
- cliente pede uma URL ou sessão de upload;
- backend valida autorização e tamanho esperado;
- cliente envia bytes crus com
PUTouPOST; - metadados vão em JSON separado;
- backend confirma checksum e promove o arquivo.
Essa abordagem reduz a complexidade no Zig. Multipart continua útil quando o formulário web tradicional é requisito, mas não precisa ser a única opção.
Respostas HTTP devem ser específicas
Upload falha de vários jeitos. Uma API madura não responde tudo com 500 ou 400 genérico.
| Situação | Código sugerido | Motivo |
|---|---|---|
| usuário sem login | 401 | autenticação ausente |
| usuário sem permissão | 403 | conta não pode enviar esse arquivo |
| arquivo grande demais | 413 | payload excede limite |
| tipo não aceito | 415 | media type inválido |
| arquivo bem formado, mas regra falhou | 422 | validação de negócio |
| upload aceito para processamento | 202 | análise assíncrona pendente |
| upload concluído | 201 | recurso criado |
Quando o arquivo passa por antivírus, extração de metadados, transcodificação ou importação de CSV, muitas vezes é melhor responder 202 Accepted e processar em background. O cliente recebe um ID de job e consulta o estado depois. Isso evita manter a conexão aberta enquanto uma tarefa pesada roda.
Observabilidade para upload
Sem métricas, upload vira caixa-preta. Registre eventos suficientes para investigar problemas sem vazar dados sensíveis.
Campos úteis:
- ID da requisição;
- ID do usuário ou conta;
- tamanho recebido;
- tipo declarado;
- tipo detectado;
- checksum;
- duração total;
- erro de validação;
- storage usado;
- decisão final: aceito, rejeitado, pendente ou removido.
Evite logar nome original quando ele pode conter dado pessoal, caminho local do usuário ou conteúdo ofensivo. Se precisar correlacionar, normalize e limite. Para mais contexto operacional, veja observabilidade em Zig e OpenTelemetry em Zig.
Armazenamento local ou objeto externo?
Para ferramenta interna pequena, arquivo local pode ser suficiente. Para produto com múltiplas instâncias, deploy em container ou alto volume, storage externo costuma ser melhor.
| Opção | Vantagem | Cuidado |
|---|---|---|
| disco local | simples, rápido, barato | backup, escala horizontal, limpeza |
| volume compartilhado | fácil para várias instâncias | lock, latência, permissões |
| S3/R2/MinIO | escala e durabilidade | credenciais, política de acesso, custo |
| fila + worker | isola processamento pesado | consistência e retries |
Zig pode participar em qualquer modelo. Em muitos sistemas, o backend Zig valida autorização, recebe ou assina o upload, calcula checksum e publica um job. Um worker separado analisa o arquivo. Isso reduz acoplamento entre HTTP e processamento pesado.
Checklist seguro para produção
Antes de publicar um endpoint de upload, revise:
- limite de tamanho no proxy e no Zig;
- autenticação antes de ler o corpo;
- autorização por conta, projeto ou recurso;
- leitura em chunks, sem carregar tudo em memória;
- arquivo temporário até validação completa;
- nome gerado pelo servidor, não pelo usuário;
- validação básica por assinatura de bytes;
- checksum incremental;
- resposta HTTP específica;
- logs e métricas sem vazar conteúdo sensível;
- limpeza de temporários antigos;
- bloqueio de execução pública do arquivo enviado;
- testes para upload incompleto, grande demais e tipo inválido.
Esse checklist é mais importante que a escolha da biblioteca. O perigo do upload não está só no parser: está no fluxo inteiro, da borda até o download posterior.
Onde Zig se destaca
Zig não transforma upload em feature automática. Ele exige que você pense em buffers, limites e erros. Para equipes que querem velocidade de desenvolvimento acima de tudo, Go, Node.js ou Python podem entregar mais rápido com bibliotecas prontas. Para serviços em que previsibilidade, binário pequeno, controle de memória e processamento de bytes importam, Zig é uma escolha forte.
Uma comparação prática: Go tem mime/multipart maduro e excelente ecossistema HTTP; o Golang Brasil cobre bem esse tipo de backend. Zig exige mais montagem manual, mas permite deixar custo e falha explícitos. Em uploads críticos, essa transparência pode ser vantagem.
Conclusão
Upload seguro é menos sobre receber bytes e mais sobre impor fronteiras. O endpoint precisa saber quem está enviando, quanto pode enviar, que tipo é aceito, onde o arquivo fica enquanto ainda não é confiável, como provar integridade e como rejeitar cedo sem derrubar o processo.
Em Zig, comece simples: limite pequeno, leitura em streaming, arquivo temporário, checksum, validação básica e resposta HTTP clara. Depois adicione multipart completo, storage externo, fila, antivírus ou processamento assíncrono conforme o produto pedir. Essa progressão mantém a API útil sem transformar o primeiro upload em uma plataforma complexa demais.