Zig HTTP POST: enviar JSON e formulários com std.http.Client

Introdução

Requisições HTTP POST são usadas para enviar dados ao servidor, como payloads JSON, formulários e comandos para uma API REST. Diferente do GET, que apenas solicita dados, o POST normalmente cria recursos, dispara uma ação ou envia um corpo que precisa ser validado no outro lado.

Nesta receita, você aprenderá a fazer HTTP POST em Zig usando std.http.Client, com Content-Type, content_length, escrita do corpo, validação de status e leitura segura da resposta. Se você quer o guia completo de cliente HTTP, leia Zig HTTP Client: GET, POST, JSON e APIs REST em Zig. Se o objetivo é receber esse POST em um serviço próprio, combine com Zig Server HTTP: API REST com std.http.Server.

Resposta rápida: POST em Zig com std.http.Client

ObjetivoCaminho em ZigPróximo link
Enviar JSON para uma API RESTclient.open(.POST, uri, ...), header Content-Type: application/json, content_length e req.write()Continue nesta receita
Gerar JSON a partir de uma structstd.json.stringify em um ArrayList(u8) antes de abrir o POSTComo gerar JSON em Zig
Fazer um GET antes do POSTclient.open(.GET, ...) e validação de statusComo fazer requisições HTTP GET em Zig
Entender a API da biblioteca padrãostd.http.Client.fetch, open, headers, TLS e storage da respostastd.http.Client em Zig
Criar a API que recebe o POSTstd.http.Server, roteamento, JSON e respostas explícitasZig Server HTTP: API REST com std.http.Server

O fluxo mínimo é sempre o mesmo: parsear a URL, abrir a requisição com método .POST, declarar os headers, definir o tamanho do corpo, chamar send(), escrever o payload, finalizar com finish(), aguardar com wait() e validar req.status antes de confiar na resposta.

Pré-requisitos

POST com Corpo JSON

O caso mais comum é enviar JSON para uma API. Em Zig, a parte importante é tornar o contrato HTTP explícito: header correto, content_length definido e status validado depois da resposta.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse("https://httpbin.org/post");

    var header_buf: [4096]u8 = undefined;
    var req = try client.open(.POST, uri, .{
        .server_header_buffer = &header_buf,
        .extra_headers = &.{
            .{ .name = "Content-Type", .value = "application/json" },
            .{ .name = "Accept", .value = "application/json" },
            .{ .name = "User-Agent", .value = "ZigBrasilPost/1.0" },
        },
    });
    defer req.deinit();

    // Definir o corpo da requisição
    const json_body =
        \\{"nome": "Maria Silva", "email": "[email protected]", "idade": 28}
    ;
    req.transfer_encoding = .{ .content_length = json_body.len };

    try req.send();

    // Escrever o corpo
    var written: usize = 0;
    while (written < json_body.len) {
        written += try req.write(json_body[written..]);
    }

    try req.finish();
    try req.wait();

    if (req.status != .ok and req.status != .created) {
        std.debug.print("Erro HTTP: {}\n", .{req.status});
        return;
    }

    // Ler resposta
    const body = try req.reader().readAllAlloc(allocator, 1024 * 1024);
    defer allocator.free(body);

    std.debug.print("Status: {}\n", .{req.status});
    std.debug.print("Resposta: {s}\n", .{body});
}

Esse exemplo usa httpbin.org apenas como destino de teste. Em produção, prefira URLs configuráveis, timeout definido na borda da aplicação e limite de resposta menor quando você conhece o tamanho esperado. O guia Zig HTTP Server em Produção cobre o outro lado desse contrato: limites de corpo, logs, health check e proxy.

Saída esperada

Status: http.Status.ok
Resposta: {
  "data": "{\"nome\": \"Maria Silva\", \"email\": \"[email protected]\", \"idade\": 28}",
  ...
}

POST com Struct Serializada para JSON

Serialize uma struct Zig diretamente para JSON antes de enviar:

const std = @import("std");

const Usuario = struct {
    nome: []const u8,
    email: []const u8,
    idade: u32,
    ativo: bool,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Criar o objeto a ser enviado
    const usuario = Usuario{
        .nome = "João Santos",
        .email = "[email protected]",
        .idade = 35,
        .ativo = true,
    };

    // Serializar para JSON
    var json_buf = std.ArrayList(u8).init(allocator);
    defer json_buf.deinit();

    try std.json.stringify(usuario, .{}, json_buf.writer());
    const json_body = json_buf.items;

    std.debug.print("JSON a enviar: {s}\n", .{json_body});

    // Enviar a requisição POST
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse("https://httpbin.org/post");

    var header_buf: [4096]u8 = undefined;
    var req = try client.open(.POST, uri, .{
        .server_header_buffer = &header_buf,
        .extra_headers = &.{
            .{ .name = "Content-Type", .value = "application/json" },
            .{ .name = "Accept", .value = "application/json" },
        },
    });
    defer req.deinit();

    req.transfer_encoding = .{ .content_length = json_body.len };

    try req.send();
    var written: usize = 0;
    while (written < json_body.len) {
        written += try req.write(json_body[written..]);
    }
    try req.finish();
    try req.wait();

    if (req.status == .ok or req.status == .created) {
        std.debug.print("Usuário criado com sucesso!\n", .{});
    } else {
        std.debug.print("Erro: {}\n", .{req.status});
    }
}

POST com Form URL-Encoded

Envie dados de formulário no formato application/x-www-form-urlencoded:

const std = @import("std");

fn urlEncode(allocator: std.mem.Allocator, params: []const [2][]const u8) ![]u8 {
    var result = std.ArrayList(u8).init(allocator);
    errdefer result.deinit();

    for (params, 0..) |param, i| {
        if (i > 0) try result.append('&');

        // Chave
        for (param[0]) |c| {
            if (std.ascii.isAlphanumeric(c) or c == '-' or c == '_' or c == '.' or c == '~') {
                try result.append(c);
            } else if (c == ' ') {
                try result.append('+');
            } else {
                try result.appendSlice(&.{ '%', std.fmt.digitToChar(c >> 4, .upper), std.fmt.digitToChar(c & 0xf, .upper) });
            }
        }

        try result.append('=');

        // Valor
        for (param[1]) |c| {
            if (std.ascii.isAlphanumeric(c) or c == '-' or c == '_' or c == '.' or c == '~') {
                try result.append(c);
            } else if (c == ' ') {
                try result.append('+');
            } else {
                try result.appendSlice(&.{ '%', std.fmt.digitToChar(c >> 4, .upper), std.fmt.digitToChar(c & 0xf, .upper) });
            }
        }
    }

    return result.toOwnedSlice();
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Codificar parâmetros do formulário
    const params = [_][2][]const u8{
        .{ "usuario", "maria_silva" },
        .{ "senha", "minha_senha_123" },
        .{ "lembrar", "true" },
    };

    const form_body = try urlEncode(allocator, &params);
    defer allocator.free(form_body);

    std.debug.print("Form body: {s}\n", .{form_body});

    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse("https://httpbin.org/post");

    var header_buf: [4096]u8 = undefined;
    var req = try client.open(.POST, uri, .{
        .server_header_buffer = &header_buf,
        .extra_headers = &.{
            .{ .name = "Content-Type", .value = "application/x-www-form-urlencoded" },
        },
    });
    defer req.deinit();

    req.transfer_encoding = .{ .content_length = form_body.len };

    try req.send();
    var written: usize = 0;
    while (written < form_body.len) {
        written += try req.write(form_body[written..]);
    }
    try req.finish();
    try req.wait();

    std.debug.print("Status: {}\n", .{req.status});
}

Função POST Reutilizável

Uma função genérica ajuda quando o POST aparece em mais de um ponto do programa. Mantenha o tamanho máximo da resposta e os status aceitos perto da função, para não espalhar política HTTP pelo código.

const std = @import("std");

const PostResponse = struct {
    status: std.http.Status,
    body: []const u8,
    allocator: std.mem.Allocator,

    pub fn deinit(self: *PostResponse) void {
        self.allocator.free(self.body);
    }
};

fn httpPost(
    allocator: std.mem.Allocator,
    url: []const u8,
    content_type: []const u8,
    payload: []const u8,
) !PostResponse {
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse(url);

    var header_buf: [8192]u8 = undefined;
    var req = try client.open(.POST, uri, .{
        .server_header_buffer = &header_buf,
        .extra_headers = &.{
            .{ .name = "Content-Type", .value = content_type },
            .{ .name = "Accept", .value = "application/json" },
            .{ .name = "User-Agent", .value = "ZigBrasilPost/1.0" },
        },
    });
    defer req.deinit();

    req.transfer_encoding = .{ .content_length = payload.len };

    try req.send();
    var written: usize = 0;
    while (written < payload.len) {
        written += try req.write(payload[written..]);
    }
    try req.finish();
    try req.wait();

    if (req.status != .ok and req.status != .created and req.status != .accepted) {
        return error.HttpStatusRejected;
    }

    const body = try req.reader().readAllAlloc(allocator, 4 * 1024 * 1024);

    return .{
        .status = req.status,
        .body = body,
        .allocator = allocator,
    };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const json =
        \\{"titulo": "Meu Post", "conteudo": "Olá mundo!"}
    ;

    var resp = try httpPost(
        allocator,
        "https://httpbin.org/post",
        "application/json",
        json,
    );
    defer resp.deinit();

    std.debug.print("Status: {}\n", .{resp.status});
    std.debug.print("Corpo ({d} bytes)\n", .{resp.body.len});
}

Dicas e Boas Práticas

  1. Sempre defina Content-Type: O servidor precisa saber o formato dos dados enviados.

  2. Defina transfer_encoding: Sempre configure o content_length antes de enviar o corpo.

  3. Defina Accept e User-Agent: APIs públicas e internas ficam mais fáceis de depurar quando o cliente declara o formato esperado e se identifica.

  4. Trate erros de rede: Conexões podem falhar. Use tratamento de erros adequado e diferencie erro de conexão, status HTTP rejeitado e JSON inválido.

  5. Valide a resposta: Verifique sempre o código de status HTTP retornado antes de fazer parse do corpo.

  6. Limite memória de resposta: readAllAlloc deve receber um teto coerente com a API. Não use limites enormes por padrão em CLIs, workers ou serviços.

  7. Serialize corretamente: Use geração de JSON para criar payloads complexos em vez de montar JSON manualmente.

  8. Conecte com o resto da aplicação: Se o POST faz parte de um backend, leia também filas e workers em background e observabilidade em Zig para decidir retry, logs e métricas.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.