---
title: "HTTP Range em Zig: 206, Content-Range e Download Retomável"
url: "https://ziglang.com.br/artigos/zig-http-range-206-download-resumivel/"
markdown_url: "https://ziglang.com.br/artigos/zig-http-range-206-download-resumivel.MD"
description: "Como servir arquivos por partes em Zig com Range, 206 Partial Content, Content-Range, Accept-Ranges, If-Range, limites e testes com curl."
date: "2026-08-17"
author: ""
---

# HTTP Range em Zig: 206, Content-Range e Download Retomável

Como servir arquivos por partes em Zig com Range, 206 Partial Content, Content-Range, Accept-Ranges, If-Range, limites e testes com curl.


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](/artigos/zig-download-arquivos-http-checksum/). Para montar o serviço completo, comece pelo guia de [servidor HTTP em Zig para produção](/artigos/zig-http-server-producao/) e aplique [ETag, If-None-Match e cache condicional](/artigos/zig-http-cache-etag-if-none-match/).

## Como uma requisição Range funciona

Um cliente que já possui os primeiros 1.000 bytes pode pedir o restante assim:

```http
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
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 é:

```text
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:

```http
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:

```zig
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:

```zig
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`: envie `200` completo;
- 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
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:

```zig
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
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:

```zig
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](/artigos/zig-upload-arquivos-validacao-streaming/) 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`:

```http
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 `Range` e envia a representação completa com `200`;
- 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:

```http
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:

1. autenticar e autorizar o acesso;
2. resolver o recurso para um caminho seguro;
3. abrir o arquivo e obter tamanho e metadados;
4. calcular `ETag` ou recuperar o validador armazenado;
5. avaliar `If-Range`;
6. interpretar e normalizar `Range` quando permitido;
7. escolher `200`, `206` ou `416`;
8. emitir headers completos;
9. transmitir exatamente o corpo prometido;
10. 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](/artigos/zig-http-compressao-gzip-brotli/) 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, `ETag` ou 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:

```text
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`, `206` e `416`;
- bytes completos versus bytes parciais;
- tamanho médio das partes;
- taxa de `If-Range` incompatí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](/artigos/zig-observabilidade-logs-prometheus/).

## Testes com curl

Crie um arquivo previsível e descubra o tamanho:

```bash
printf '0123456789abcdefghijklmnopqrstuvwxyz' > exemplo.txt
wc -c exemplo.txt
```

Peça os dez primeiros bytes:

```bash
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:

```bash
curl -i \
  -H 'Range: bytes=10-' \
  http://localhost:8080/exemplo.txt
```

Peça os últimos seis bytes:

```bash
curl -i \
  -H 'Range: bytes=-6' \
  http://localhost:8080/exemplo.txt
```

Teste um intervalo impossível:

```bash
curl -i \
  -H 'Range: bytes=999-' \
  http://localhost:8080/exemplo.txt
```

Espere `416` e `Content-Range: bytes */36`.

Para simular retomada em disco:

```bash
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-0` retorna exatamente um byte;
- [ ] o fim é limitado a `file_size - 1`;
- [ ] início fora do recurso retorna `416` com `bytes */total`;
- [ ] `206` inclui `Content-Range`, `Accept-Ranges` e tamanho da parte;
- [ ] o loop transmite no máximo a parte autorizada;
- [ ] `If-Range` impede 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`, `206` e `416`.

## 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](/artigos/zig-graceful-shutdown-sigterm-sigint/) e a [health checks de readiness e liveness](/artigos/zig-health-check-readiness-liveness/), evitando iniciar novos downloads durante a drenagem do serviço.
