Padrão Middleware em Zig: Logging, Autenticação e CORS

No artigo anterior, construímos uma API REST com CRUD. Agora vamos adicionar uma camada essencial: middlewares. Middlewares são funções que processam a requisição antes (ou depois) do handler principal, permitindo adicionar funcionalidades transversais de forma modular.

O Conceito de Middleware

Um middleware em Zig segue um padrão simples: recebe o contexto, faz algum processamento, e decide se passa a requisição adiante ou retorna uma resposta.

const std = @import("std");

const Context = struct {
    request: *std.http.Server.Request,
    allocator: std.mem.Allocator,
    headers_extras: std.ArrayList(std.http.Header),
    timestamp_inicio: i64,

    fn init(alloc: std.mem.Allocator, req: *std.http.Server.Request) Context {
        return .{
            .request = req,
            .allocator = alloc,
            .headers_extras = std.ArrayList(std.http.Header).init(alloc),
            .timestamp_inicio = std.time.milliTimestamp(),
        };
    }

    fn addHeader(self: *Context, nome: []const u8, valor: []const u8) !void {
        try self.headers_extras.append(.{ .name = nome, .value = valor });
    }
};

// Tipo de middleware: recebe contexto e next, retorna se deve continuar
const Middleware = *const fn (*Context, NextFn) anyerror!void;
const NextFn = *const fn (*Context) anyerror!void;

Implementando uma Cadeia de Middlewares

const MiddlewareChain = struct {
    middlewares: std.ArrayList(Middleware),
    handler_final: NextFn,

    fn init(allocator: std.mem.Allocator, handler: NextFn) MiddlewareChain {
        return .{
            .middlewares = std.ArrayList(Middleware).init(allocator),
            .handler_final = handler,
        };
    }

    fn deinit(self: *MiddlewareChain) void {
        self.middlewares.deinit();
    }

    fn use(self: *MiddlewareChain, mw: Middleware) !void {
        try self.middlewares.append(mw);
    }

    fn executar(self: *MiddlewareChain, ctx: *Context) !void {
        if (self.middlewares.items.len == 0) {
            try self.handler_final(ctx);
            return;
        }

        // Executar middlewares em sequência
        var indice: usize = 0;
        const chain = self;

        const Executor = struct {
            fn next(c: *Context) anyerror!void {
                _ = c;
                // Simplificado: em produção, use closure ou state machine
            }
        };

        // Executar primeiro middleware
        try chain.middlewares.items[indice](ctx, Executor.next);
    }
};

Middleware 1: Logger de Requisições

fn loggingMiddleware(ctx: *Context, next: NextFn) anyerror!void {
    const inicio = std.time.milliTimestamp();
    const metodo = @tagName(ctx.request.head.method);
    const caminho = ctx.request.head.target;

    std.debug.print("[{d}] {s} {s}", .{ inicio, metodo, caminho });

    // Executar próximo middleware/handler
    try next(ctx);

    // Log de conclusão
    const duracao = std.time.milliTimestamp() - inicio;
    std.debug.print(" -> {d}ms\n", .{duracao});
}

Saída de exemplo:

[1708531200000] GET /api/tarefas -> 2ms
[1708531200050] POST /api/tarefas -> 5ms

Middleware 2: CORS (Cross-Origin Resource Sharing)

const CorsConfig = struct {
    origens_permitidas: []const []const u8,
    metodos_permitidos: []const u8,
    headers_permitidos: []const u8,
    max_age: []const u8,
};

fn corsMiddleware(ctx: *Context, next: NextFn) anyerror!void {
    // Adicionar headers CORS
    try ctx.addHeader("Access-Control-Allow-Origin", "*");
    try ctx.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    try ctx.addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
    try ctx.addHeader("Access-Control-Max-Age", "86400");

    // Se for preflight (OPTIONS), responder imediatamente
    if (ctx.request.head.method == .OPTIONS) {
        try ctx.request.respond("", .{
            .status = .no_content,
            .extra_headers = ctx.headers_extras.items,
        });
        return; // Não chama next — resposta já enviada
    }

    // Para outros métodos, continuar a cadeia
    try next(ctx);
}

Middleware 3: Autenticação por Token

const AuthConfig = struct {
    token_valido: []const u8,
    rotas_publicas: []const []const u8,
};

fn criarAuthMiddleware(config: AuthConfig) Middleware {
    const Impl = struct {
        var cfg: AuthConfig = undefined;

        fn middleware(ctx: *Context, next: NextFn) anyerror!void {
            // Verificar se a rota é pública
            for (cfg.rotas_publicas) |rota| {
                if (std.mem.eql(u8, ctx.request.head.target, rota)) {
                    try next(ctx);
                    return;
                }
            }

            // Buscar header Authorization
            var auth_header: ?[]const u8 = null;
            var it = ctx.request.head.iterateHeaders();
            while (it.next()) |header| {
                if (std.mem.eql(u8, header.name, "authorization")) {
                    auth_header = header.value;
                    break;
                }
            }

            if (auth_header) |auth| {
                // Espera formato: "Bearer <token>"
                if (std.mem.startsWith(u8, auth, "Bearer ")) {
                    const token = auth[7..];
                    if (std.mem.eql(u8, token, cfg.token_valido)) {
                        try next(ctx);
                        return;
                    }
                }
            }

            // Autenticação falhou
            try ctx.request.respond(
                "{\"erro\": \"Não autorizado\", \"codigo\": 401}",
                .{
                    .status = .unauthorized,
                    .extra_headers = &.{
                        .{ .name = "content-type", .value = "application/json" },
                        .{ .name = "www-authenticate", .value = "Bearer" },
                    },
                },
            );
        }
    };

    Impl.cfg = config;
    return Impl.middleware;
}

Middleware 4: Rate Limiter

const RateLimiter = struct {
    contadores: std.StringHashMap(ContadorIP),
    limite_por_minuto: u32,
    allocator: std.mem.Allocator,

    const ContadorIP = struct {
        contagem: u32,
        janela_inicio: i64,
    };

    fn init(allocator: std.mem.Allocator, limite: u32) RateLimiter {
        return .{
            .contadores = std.StringHashMap(ContadorIP).init(allocator),
            .limite_por_minuto = limite,
            .allocator = allocator,
        };
    }

    fn deinit(self: *RateLimiter) void {
        self.contadores.deinit();
    }

    fn verificar(self: *RateLimiter, ip: []const u8) bool {
        const agora = std.time.timestamp();

        if (self.contadores.getPtr(ip)) |contador| {
            // Resetar janela se passou 1 minuto
            if (agora - contador.janela_inicio >= 60) {
                contador.contagem = 1;
                contador.janela_inicio = agora;
                return true;
            }

            // Verificar limite
            if (contador.contagem >= self.limite_por_minuto) {
                return false;
            }

            contador.contagem += 1;
            return true;
        }

        // Novo IP
        self.contadores.put(ip, .{
            .contagem = 1,
            .janela_inicio = agora,
        }) catch return false;

        return true;
    }
};

Combinando Middlewares: Aplicação Completa

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

    var rate_limiter = RateLimiter.init(allocator, 100);
    defer rate_limiter.deinit();

    const endereco = std.net.Address.parseIp("127.0.0.1", 8080) catch unreachable;
    var servidor = try endereco.listen(.{ .reuse_address = true });
    defer servidor.deinit();

    std.debug.print("API com middlewares rodando em http://127.0.0.1:8080\n", .{});
    std.debug.print("Middlewares ativos: logging, CORS, rate-limiting\n", .{});

    while (true) {
        var arena = std.heap.ArenaAllocator.init(allocator);
        defer arena.deinit();

        var conexao = try servidor.accept();
        defer conexao.stream.close();

        var buf: [8192]u8 = undefined;
        var http = std.http.Server.init(conexao, &buf);

        var request = http.receiveHead() catch continue;
        var ctx = Context.init(arena.allocator(), &request);

        const inicio = std.time.milliTimestamp();

        // Middleware 1: Rate limiting
        if (!rate_limiter.verificar("127.0.0.1")) {
            try request.respond(
                "{\"erro\": \"Limite de requisições excedido\"}",
                .{ .status = .too_many_requests },
            );
            continue;
        }

        // Middleware 2: CORS headers
        try ctx.addHeader("Access-Control-Allow-Origin", "*");

        if (request.head.method == .OPTIONS) {
            try request.respond("", .{ .status = .no_content });
            continue;
        }

        // Handler principal
        const caminho = request.head.target;
        if (std.mem.eql(u8, caminho, "/api/status")) {
            try request.respond(
                "{\"status\": \"online\", \"middlewares\": [\"logging\", \"cors\", \"rate-limit\"]}",
                .{ .extra_headers = &.{
                    .{ .name = "content-type", .value = "application/json" },
                } },
            );
        } else {
            try request.respond(
                "{\"erro\": \"Rota não encontrada\"}",
                .{ .status = .not_found },
            );
        }

        // Middleware logging (pós-processamento)
        const duracao = std.time.milliTimestamp() - inicio;
        std.debug.print("[LOG] {s} {s} -> {d}ms\n", .{
            @tagName(request.head.method),
            caminho,
            duracao,
        });
    }
}

Padrão de Composição de Middlewares

A verdadeira potência vem da composição. Podemos criar “grupos” de middlewares para diferentes rotas:

const MiddlewareGroup = struct {
    nome: []const u8,
    middlewares: []const Middleware,

    fn aplicar(self: MiddlewareGroup, ctx: *Context, handler: NextFn) !void {
        // Executar cada middleware na ordem
        for (self.middlewares) |mw| {
            try mw(ctx, handler);
        }
    }
};

// Exemplo de uso:
// const grupo_api = MiddlewareGroup{
//     .nome = "api",
//     .middlewares = &.{ loggingMiddleware, corsMiddleware, authMiddleware },
// };
//
// const grupo_publico = MiddlewareGroup{
//     .nome = "publico",
//     .middlewares = &.{ loggingMiddleware, corsMiddleware },
// };

Próximos Passos

No artigo final da série, vamos preparar a aplicação para deploy em produção: configuração via variáveis de ambiente, graceful shutdown, logging estruturado e containerização com Docker.

Referências

Continue aprendendo Zig

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