Proxying
January 20, 2026 ยท View on GitHub
The most common edge computing pattern: accept a request, optionally modify it, send it to an origin server, and return the response.
Simple Proxy
The proxy() method handles the entire flow:
const zigly = @import("zigly");
fn start() !void {
var downstream = try zigly.downstream();
try downstream.proxy("origin", "api.example.com");
}
This:
- Sets the Host header to
api.example.com - Forwards the request to the "origin" backend
- Returns the backend's response to the client
The second argument (host_header) can be null to preserve the original Host header.
Modifying Requests
Manipulate the request before proxying:
fn start() !void {
var downstream = try zigly.downstream();
var request = downstream.request;
// Add authentication
try request.headers.set("Authorization", "Bearer secret-token");
// Remove sensitive headers from reaching origin
try request.headers.remove("Cookie");
// Add tracking header
try request.headers.set("X-Edge-Location", "SFO");
try downstream.proxy("origin", "api.example.com");
}
Conditional Proxying
Route to different backends based on request properties:
const std = @import("std");
const zigly = @import("zigly");
fn start() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const alloc = arena.allocator();
var downstream = try zigly.downstream();
var uri_buf: [4096]u8 = undefined;
const uri = try downstream.request.getUriString(&uri_buf);
// Route based on path prefix
if (std.mem.startsWith(u8, uri, "/api/")) {
try downstream.proxy("api_backend", "api.example.com");
} else if (std.mem.startsWith(u8, uri, "/static/")) {
try downstream.proxy("storage_backend", "cdn.example.com");
} else {
try downstream.proxy("web_backend", "www.example.com");
}
}
Manual Request/Response
For more control, handle requests and responses separately:
const zigly = @import("zigly");
const Request = zigly.http.Request;
fn start() !void {
var downstream = try zigly.downstream();
// Create upstream request
var uri_buf: [4096]u8 = undefined;
const client_uri = try downstream.request.getUriString(&uri_buf);
var upstream_req = try Request.new("GET", client_uri);
try upstream_req.headers.set("Host", "api.example.com");
// Copy specific headers from client
if (downstream.request.headers.get(alloc, "Accept")) |accept| {
try upstream_req.headers.set("Accept", accept);
} else |_| {}
// Send to backend
var upstream_resp = try upstream_req.send("origin");
// Modify response before sending to client
try downstream.response.setStatus(try upstream_resp.getStatus());
try downstream.response.headers.set("X-Served-By", "edge");
// Copy body
var body_buf: [8192]u8 = undefined;
while (true) {
const chunk = try upstream_resp.body.read(&body_buf);
if (chunk.len == 0) break;
try downstream.response.body.writeAll(chunk);
}
try downstream.response.finish();
}
Zero-Copy Piping
Use pipe() for efficient response forwarding:
fn start() !void {
var downstream = try zigly.downstream();
var upstream_req = try Request.new("GET", "/data");
var upstream_resp = try upstream_req.send("origin");
// Zero-copy the response
// Arguments: copy_status, copy_headers
try downstream.response.pipe(&upstream_resp, true, true);
}
pipe() directly streams the upstream response to the client without buffering.
Redirects
Redirect clients without proxying:
fn start() !void {
var downstream = try zigly.downstream();
// Permanent redirect
try downstream.redirect(301, "https://new.example.com/page");
// Temporary redirect
// try downstream.redirect(302, "/temporary-location");
}
Caching Policy
Control caching behavior per-request:
fn start() !void {
var downstream = try zigly.downstream();
// Bypass cache for this request
try downstream.request.setCachingPolicy(.{ .no_cache = true });
try downstream.proxy("origin", null);
}
fn start() !void {
var downstream = try zigly.downstream();
// Override TTL and enable stale-while-revalidate
try downstream.request.setCachingPolicy(.{
.ttl = 300, // 5 minutes
.serve_stale = 3600, // Serve stale for 1 hour if origin fails
});
try downstream.proxy("origin", null);
}
Dynamic Backends
Create backends at runtime:
const zigly = @import("zigly");
const DynamicBackend = zigly.DynamicBackend;
fn start() !void {
var downstream = try zigly.downstream();
// Register a new backend
const backend = try (DynamicBackend{
.name = "dynamic_origin",
.target = "dynamic.example.com:443",
.use_ssl = true,
.host_override = "dynamic.example.com",
.sni_hostname = "dynamic.example.com",
.cert_hostname = "dynamic.example.com",
.connect_timeout_ms = 5000,
.first_byte_timeout_ms = 15000,
.between_bytes_timeout_ms = 10000,
}).register();
try downstream.proxy(backend.name, null);
}
Failover
Implement backend failover:
fn start() !void {
var downstream = try zigly.downstream();
// Try primary backend
downstream.proxy("primary", "primary.example.com") catch {
// Fall back to secondary
downstream.proxy("secondary", "secondary.example.com") catch {
// Both failed, return error
try downstream.response.setStatus(503);
try downstream.response.body.writeAll("Service unavailable");
try downstream.response.finish();
return;
};
};
}
Auto-Decompression
Handle compressed responses from backends:
fn start() !void {
var downstream = try zigly.downstream();
// Enable automatic gzip decompression
try downstream.request.setAutoDecompressResponse(true);
var upstream_resp = try downstream.request.send("origin");
// Response body is now decompressed
}
Request Logging
Log requests in Apache format:
fn start() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const alloc = arena.allocator();
var downstream = try zigly.downstream();
// Process request and capture response details
try downstream.proxy("origin", null);
// Log in Apache combined format
try downstream.request.logApacheCombined(alloc, "access_log", 200, 1234);
}
Next Steps
- Caching - Cache responses at the edge
- Rate Limiting - Protect your backends
- Backend Reference - Backend API details