Para implementar HTTP Range em Zig, aceite inicialmente um único intervalo no formato bytes=início-fim, valide-o contra o tamanho atual do arquivo e responda com 206 Partial Content, Content-Range, Accept-Ranges: bytes e o Content-Length da parte. Depois, faça seek até o byte inicial e copie somente a quantidade autorizada em blocos fixos. Se o início estiver além do fim do arquivo, responda 416 Range Not Satisfiable com Content-Range: bytes */tamanho.
Esse mecanismo permite retomar downloads interrompidos, avançar em vídeos e áudios e buscar trechos de artefatos grandes sem transferir o recurso inteiro. Porém, Range não resolve sozinho a consistência: combine-o com um ETag forte e If-Range para evitar juntar bytes de versões diferentes do mesmo arquivo.
Resposta rápida
| Situação | Resposta recomendada |
|---|---|
Requisição sem Range | 200 OK com o arquivo completo |
| Intervalo válido | 206 Partial Content |
| Início além do tamanho | 416 Range Not Satisfiable |
| Parte servida | Content-Range: bytes início-fim/total |
| Capacidade anunciada | Accept-Ranges: bytes |
| Tamanho do corpo parcial | Content-Length: fim - início + 1 |
Recurso mudou e If-Range não bate | ignorar Range e enviar 200 completo |
| Arquivo grande | seek + cópia limitada em chunks |
| Vários intervalos | adiar até haver necessidade real |
| Teste básico | curl -H 'Range: bytes=0-99' |
Se o seu objetivo é baixar um arquivo do lado cliente, veja download HTTP com checksum e escrita atômica. Para montar o serviço completo, comece pelo guia de servidor HTTP em Zig para produção e aplique ETag, If-None-Match e cache condicional.
Como uma requisição Range funciona
Um cliente que já possui os primeiros 1.000 bytes pode pedir o restante assim:
GET /releases/app-linux-x86_64.tar.zst HTTP/1.1
Host: downloads.exemplo.com.br
Range: bytes=1000-
Se o arquivo tem 50.000 bytes, o servidor responde:
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: application/zstd
Content-Range: bytes 1000-49999/50000
Content-Length: 49000
ETag: "sha256:abc123..."
...bytes 1000 até 49999...
Os offsets são inclusivos. Portanto, a quantidade enviada é:
comprimento = fim - início + 1
Esquecer o + 1 é um erro clássico. O intervalo bytes=0-0 solicita exatamente um byte, não zero bytes. O intervalo bytes=0-99 solicita 100 bytes.
Há três formas simples que um parser inicial precisa reconhecer:
Range: bytes=0-499
Range: bytes=500-
Range: bytes=-500
A primeira pede um intervalo fechado. A segunda pede do offset 500 até o fim. A terceira pede os últimos 500 bytes e é chamada de suffix range. Um header também pode pedir vários intervalos, como bytes=0-99,200-299, mas isso produz uma resposta multipart/byteranges mais complexa. Não é necessário começar por ela.
Modele o intervalo normalizado
É mais seguro separar o texto recebido do intervalo que será usado para abrir e transmitir o arquivo. Depois da validação, o restante do handler deve lidar apenas com offsets concretos:
const ByteRange = struct {
start: u64,
end: u64, // inclusivo
pub fn len(self: ByteRange) u64 {
return self.end - self.start + 1;
}
};
const RangeError = error{
InvalidUnit,
InvalidSyntax,
MultipleRangesUnsupported,
Unsatisfiable,
Overflow,
};
A função de normalização recebe o valor do header e o tamanho atual do recurso. Ela nunca deve produzir start > end, end >= file_size ou uma soma que estoure o tipo inteiro.
Uma implementação didática pode seguir este desenho:
fn normalizeRange(value: []const u8, file_size: u64) RangeError!ByteRange {
if (file_size == 0) return RangeError.Unsatisfiable;
if (!std.mem.startsWith(u8, value, "bytes=")) return RangeError.InvalidUnit;
const spec = std.mem.trim(u8, value["bytes=".len..], " \t");
if (std.mem.indexOfScalar(u8, spec, ',') != null)
return RangeError.MultipleRangesUnsupported;
const dash = std.mem.indexOfScalar(u8, spec, '-') orelse
return RangeError.InvalidSyntax;
const left = std.mem.trim(u8, spec[0..dash], " \t");
const right = std.mem.trim(u8, spec[dash + 1 ..], " \t");
if (left.len == 0) {
const suffix = std.fmt.parseUnsigned(u64, right, 10) catch
return RangeError.InvalidSyntax;
if (suffix == 0) return RangeError.Unsatisfiable;
const count = @min(suffix, file_size);
return .{ .start = file_size - count, .end = file_size - 1 };
}
const start = std.fmt.parseUnsigned(u64, left, 10) catch
return RangeError.InvalidSyntax;
if (start >= file_size) return RangeError.Unsatisfiable;
const requested_end = if (right.len == 0)
file_size - 1
else
std.fmt.parseUnsigned(u64, right, 10) catch
return RangeError.InvalidSyntax;
if (requested_end < start) return RangeError.Unsatisfiable;
return .{
.start = start,
.end = @min(requested_end, file_size - 1),
};
}
A API exata de parsing e resposta HTTP pode variar entre versões do Zig anteriores ao 1.0. Confira zig version e as assinaturas instaladas de std.http.Server. O contrato do protocolo, os limites e os testes continuam válidos mesmo quando os nomes da stdlib mudam.
Escolha uma política para headers inválidos
O protocolo distingue um intervalo impossível de uma sintaxe que o servidor não entende. Na prática, você precisa de uma política consistente:
- sem header
Range: envie200completo; - intervalo único válido: envie
206; - intervalo válido, mas fora do recurso: envie
416; - unidade desconhecida ou sintaxe inválida: ignore o header e envie
200, ou responda erro conforme o contrato da aplicação; - múltiplos ranges não suportados: ignore e envie
200, ou rejeite explicitamente se clientes e documentação concordarem.
Ignorar um Range malformado e servir a representação completa é interoperável em muitos cenários, mas pode gastar muita banda. Para endpoints de artefatos gigantes, uma rejeição clara pode ser operacionalmente melhor. O importante é não deixar uma falha de parsing virar offset incorreto, alocação descontrolada ou leitura fora do arquivo.
Monte corretamente a resposta 206
Suponha um arquivo de 10.000 bytes e o pedido bytes=2000-2999. A resposta precisa informar tanto a parte quanto o total:
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 2000-2999/10000
Content-Length: 1000
Content-Type: application/octet-stream
Não reutilize o tamanho total no Content-Length: ele descreve o corpo desta resposta, que tem 1.000 bytes. Também não envie 206 sem Content-Range, pois o cliente precisa saber onde encaixar a parte.
Uma função pequena ajuda a formatar o valor sem espalhar aritmética pelo handler:
fn formatContentRange(
buffer: []u8,
range: ByteRange,
total: u64,
) ![]const u8 {
return std.fmt.bufPrint(
buffer,
"bytes {d}-{d}/{d}",
.{ range.start, range.end, total },
);
}
Para 416, o formato muda:
HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */10000
Content-Length: 0
Esse total permite que um downloader descubra que seu arquivo temporário local ficou maior que o recurso atual e decida recomeçar.
Transmita sem carregar a parte inteira
Depois de validar o intervalo, abra o arquivo, faça seek e copie exatamente range.len() bytes. Um loop com orçamento restante evita ultrapassar o final autorizado mesmo se o reader puder fornecer mais dados:
fn copyFileRange(file: *std.fs.File, writer: anytype, range: ByteRange) !void {
try file.seekTo(range.start);
var buffer: [32 * 1024]u8 = undefined;
var remaining = range.len();
while (remaining > 0) {
const wanted: usize = @intCast(@min(remaining, buffer.len));
const n = try file.read(buffer[0..wanted]);
if (n == 0) return error.UnexpectedEndOfFile;
try writer.writeAll(buffer[0..n]);
remaining -= n;
}
}
O uso de memória fica em aproximadamente 32 KiB por transmissão, independentemente de o cliente pedir 1 MiB ou 20 GiB. Escolha o buffer por benchmark; 16, 32 ou 64 KiB são pontos de partida, não verdades universais.
Abra e examine o arquivo com cuidado para evitar uma corrida entre a leitura dos metadados e a transmissão. Quando possível, obtenha o tamanho do mesmo descritor que será usado para ler. Não derive o caminho diretamente de uma URL sem normalização, pois sequências como ../ podem escapar do diretório permitido. O guia de upload seguro em Zig discute a mesma fronteira pelo lado da entrada.
Use ETag e If-Range para consistência
Imagine que o cliente baixou os bytes 0-999 da versão A. Antes da retomada, o servidor publica a versão B, com conteúdo diferente. Se o cliente pedir apenas bytes=1000-, poderá concatenar o prefixo de A com o restante de B e produzir um arquivo corrompido.
A solução é guardar um validador forte recebido anteriormente e enviá-lo em If-Range:
GET /release.tar.zst HTTP/1.1
Range: bytes=1000-
If-Range: "sha256:abc123..."
O servidor compara o If-Range com o ETag atual:
- se corresponder, atende o intervalo com
206; - se não corresponder, ignora
Rangee envia a representação completa com200; - o cliente substitui o temporário antigo em vez de anexar bytes incompatíveis.
Para downloads que precisam de integridade, o ETag pode vir de um hash do artefato ou de outro identificador forte e estável da versão. Evite um validador fraco para montagem byte a byte. Além disso, publique um checksum separado quando o arquivo é distribuído como release; o cliente deve verificar o resultado final antes de renomear o .part para o destino definitivo.
If-Range é diferente de If-None-Match. O primeiro decide entre uma parte e o corpo completo. O segundo pode resultar em 304 Not Modified, sem corpo. Mantenha essas duas decisões separadas no código.
Não implemente multipart/byteranges cedo demais
Um cliente pode enviar:
Range: bytes=0-99,1000-1099
Atender isso corretamente exige uma resposta multipart/byteranges, em que cada parte tem seu próprio Content-Range e o corpo usa um boundary. O Content-Length total fica mais trabalhoso, e uma lista enorme de pequenos intervalos pode aumentar CPU, seeks, overhead de headers e complexidade de testes.
Para uma primeira versão de um servidor de artefatos, suportar apenas um intervalo costuma cobrir download retomável e muitos players. Documente a limitação, limite o tamanho do header e adicione múltiplos ranges somente quando métricas ou clientes reais exigirem.
Se implementar depois, imponha pelo menos:
- quantidade máxima de intervalos;
- soma máxima de bytes solicitados;
- normalização e, se adequado, união de intervalos sobrepostos;
- limite de tempo por resposta;
- proteção contra headers gigantes;
- testes de boundaries e conteúdo binário.
Cabeçalhos e decisões que não podem se contradizer
Uma resposta parcial precisa continuar descrevendo o mesmo recurso. Preserve Content-Type, Content-Disposition quando houver download com nome sugerido, ETag, Last-Modified e políticas de cache coerentes.
Evite combinar decisões em ordem aleatória. Um pipeline previsível pode ser:
- autenticar e autorizar o acesso;
- resolver o recurso para um caminho seguro;
- abrir o arquivo e obter tamanho e metadados;
- calcular
ETagou recuperar o validador armazenado; - avaliar
If-Range; - interpretar e normalizar
Rangequando permitido; - escolher
200,206ou416; - emitir headers completos;
- transmitir exatamente o corpo prometido;
- registrar status, offsets, bytes e duração.
Se houver compressão dinâmica, seja especialmente cuidadoso. Ranges normalmente se referem aos bytes da representação selecionada. Comprimir o arquivo durante a resposta muda offsets e tamanho. Para artefatos já comprimidos, como .zip, .gz e .zst, sirva os bytes armazenados sem recompressão. Para outros recursos, é comum desativar compressão em respostas parciais ou manter variantes pré-comprimidas com validadores próprios. Leia compressão HTTP em Zig antes de misturar as duas funcionalidades.
Segurança e limites operacionais
Range economiza banda para clientes legítimos, mas também permite gerar muitas leituras pequenas. Trate o header como entrada não confiável.
Aplique limites em camadas:
- tamanho máximo do header;
- um intervalo por requisição na primeira versão;
- tamanho mínimo de parte, se o produto permitir;
- rate limiting por cliente ou credencial;
- número máximo de conexões simultâneas;
- timeout de escrita para clientes lentos;
- raiz de arquivos fixa e caminhos normalizados;
- autorização antes de revelar tamanho,
ETagou existência; - logs sem tokens, cookies ou URLs assinadas completas.
Para conteúdo privado, não use a resposta 416 como um oráculo que revela o tamanho de um recurso antes da autorização. Primeiro confirme que o cliente pode acessar o arquivo; só depois avalie o intervalo.
Também considere o que acontece se o arquivo for truncado durante a transmissão. O loop deve detectar EOF prematuro, encerrar a resposta e registrar a divergência. Uma estratégia de publicação atômica — escrever uma versão temporária e renomeá-la quando completa — reduz esse risco.
Observabilidade útil
Não registre cada chunk. Registre uma linha estruturada por requisição com campos que ajudem a distinguir uso normal de abuso:
method=GET status=206 resource=release-42 range_start=1048576 range_end=5242879 bytes=4194304 etag_match=true duration_ms=183
Métricas úteis incluem:
- quantidade de respostas
200,206e416; - bytes completos versus bytes parciais;
- tamanho médio das partes;
- taxa de
If-Rangeincompatível; - duração e throughput das transmissões;
- conexões encerradas antes do fim;
- número de pedidos com múltiplos ranges rejeitados;
- recursos mais retomados.
Uma alta repentina de 416 pode indicar bug no downloader, arquivo trocado sem controle de versão ou tentativa automatizada de sondagem. Uma alta de partes minúsculas pode justificar rate limit específico. Combine esses sinais com o guia de logs e métricas Prometheus em Zig.
Testes com curl
Crie um arquivo previsível e descubra o tamanho:
printf '0123456789abcdefghijklmnopqrstuvwxyz' > exemplo.txt
wc -c exemplo.txt
Peça os dez primeiros bytes:
curl -i \
-H 'Range: bytes=0-9' \
http://localhost:8080/exemplo.txt
Espere 206, Content-Range: bytes 0-9/36, Content-Length: 10 e o corpo 0123456789.
Peça do byte 10 até o fim:
curl -i \
-H 'Range: bytes=10-' \
http://localhost:8080/exemplo.txt
Peça os últimos seis bytes:
curl -i \
-H 'Range: bytes=-6' \
http://localhost:8080/exemplo.txt
Teste um intervalo impossível:
curl -i \
-H 'Range: bytes=999-' \
http://localhost:8080/exemplo.txt
Espere 416 e Content-Range: bytes */36.
Para simular retomada em disco:
curl -o arquivo.part \
-H 'Range: bytes=0-999999' \
http://localhost:8080/release.tar.zst
curl -C - -o arquivo.part \
http://localhost:8080/release.tar.zst
Além do teste manual, cubra o normalizador com testes unitários:
| Entrada | Tamanho | Resultado |
|---|---|---|
bytes=0-0 | 100 | 0..0 |
bytes=0-99 | 100 | 0..99 |
bytes=50- | 100 | 50..99 |
bytes=-10 | 100 | 90..99 |
bytes=-200 | 100 | 0..99 |
bytes=99-200 | 100 | 99..99 |
bytes=100- | 100 | 416 |
bytes=20-10 | 100 | 416 |
items=0-10 | 100 | unidade não suportada |
bytes=0-1,4-5 | 100 | múltiplos não suportados |
Inclua arquivo vazio, números maiores que u64, espaços inesperados, campos vazios e desconexão durante a escrita.
Checklist de produção
Antes de publicar um endpoint com Range, confirme:
- o acesso ao recurso é autorizado antes de avaliar o intervalo;
- o caminho fica preso à raiz permitida;
- o tamanho vem do arquivo realmente aberto;
- os offsets usam inteiros capazes de representar arquivos grandes;
-
bytes=0-0retorna exatamente um byte; - o fim é limitado a
file_size - 1; - início fora do recurso retorna
416combytes */total; -
206incluiContent-Range,Accept-Rangese tamanho da parte; - o loop transmite no máximo a parte autorizada;
-
If-Rangeimpede mistura de versões; - publicação de arquivos é atômica;
- múltiplos ranges têm política explícita;
- timeouts, concorrência e rate limit estão definidos;
- testes verificam headers e bytes do corpo;
- métricas distinguem
200,206e416.
Perguntas frequentes
O que um servidor Zig deve responder a um Range válido?
Ele deve responder 206 Partial Content, informar Accept-Ranges: bytes, enviar Content-Range com o intervalo efetivamente servido e o tamanho total, definir Content-Length como o tamanho da parte e transmitir somente os bytes solicitados.
Quando a resposta deve ser 416 Range Not Satisfiable?
Use 416 quando o intervalo não pode ser satisfeito, por exemplo quando o byte inicial é igual ou maior que o tamanho atual do arquivo. A resposta deve incluir Content-Range: bytes */tamanho para informar o comprimento vigente do recurso.
Range permite retomar qualquer download com segurança?
Não sozinho. Para impedir que o cliente combine partes de versões diferentes, associe a retomada a um validador forte, normalmente ETag, e use If-Range. Se o validador não corresponder, envie o arquivo completo com 200 em vez de uma parte 206.
Preciso implementar múltiplos ranges na primeira versão?
Não. Um servidor simples pode suportar apenas um intervalo e rejeitar ou ignorar pedidos multipart conforme uma política documentada. Múltiplos ranges exigem multipart/byteranges, boundaries, mais validação e proteção contra abuso.
Devo carregar a parte solicitada inteira na memória?
Não para arquivos grandes. Faça seek até o offset inicial e copie no máximo a quantidade calculada em blocos fixos. Assim, o uso de memória permanece limitado mesmo quando a parte tem centenas de megabytes.
Conclusão
Suporte a HTTP Range não exige um framework pesado, mas exige aritmética, headers e estados coerentes. Normalize um único intervalo, responda 206 com metadados exatos, use 416 quando a parte não existe e transmita por streaming com um orçamento de bytes. Em seguida, adicione ETag e If-Range para que retomada signifique continuidade do mesmo arquivo, não apenas concatenação cega.
Esse desenho combina bem com Zig: offsets, buffers, erros e limites ficam visíveis. Comece pequeno, teste cada formato de intervalo e só implemente multipart/byteranges quando houver demanda concreta. Para completar a cadeia, conecte o endpoint a graceful shutdown com SIGTERM e a health checks de readiness e liveness, evitando iniciar novos downloads durante a drenagem do serviço.