HTTP Reference

July 2, 2026 ยท View on GitHub

The HTTP module provides types for handling requests and responses.

downstream()

Get the client connection:

const zigly = @import("zigly");

var downstream = try zigly.downstream();
// downstream.request  - The incoming request
// downstream.response - Your outgoing response

Returns a Downstream struct.


Downstream

The connection from the client to your edge service.

Fields

FieldTypeDescription
requestRequestThe incoming HTTP request
responseOutgoingResponseThe response to send back

Methods

proxy

pub fn proxy(self: *Downstream, backend: []const u8, host_header: ?[]const u8) !void

Proxy the request to a backend and send the response to the client.

  • backend - Name of the backend to send to
  • host_header - Optional Host header value. Pass null to preserve the original.
try downstream.proxy("origin", "api.example.com");

redirect

pub fn redirect(self: *Downstream, status: u16, uri: []const u8) !void

Send an HTTP redirect response.

  • status - HTTP status code (301, 302, 307, 308)
  • uri - Redirect destination
try downstream.redirect(301, "https://example.com/new-path");

getClientIpAddr

pub fn getClientIpAddr() !geo.Ip

Get the client's IP address.

const ip = try Downstream.getClientIpAddr();
const ip_str = try ip.print(allocator);

Request

An HTTP request, either from the client or created programmatically.

Fields

FieldTypeDescription
headersRequestHeadersRequest headers
bodyBodyRequest body

Static Methods

downstream

pub fn downstream() !Request

Get the incoming client request. Typically accessed via zigly.downstream().request.

new

pub fn new(method: []const u8, uri: []const u8) !Request

Create a new request for sending to a backend.

var req = try Request.new("POST", "https://api.example.com/data");
try req.headers.set("Content-Type", "application/json");
try req.body.writeAll("{\"key\":\"value\"}");
var resp = try req.send("api_backend");

Instance Methods

getMethod

pub fn getMethod(self: Request, method: []u8) ![]u8

Copy the HTTP method to the provided buffer.

var buf: [16]u8 = undefined;
const method = try request.getMethod(&buf);  // "GET", "POST", etc.

isGet

pub fn isGet(self: Request) !bool

Check if the request method is GET.

isPost

pub fn isPost(self: Request) !bool

Check if the request method is POST.

setMethod

pub fn setMethod(self: Request, method: []const u8) !void

Set the request method.

try request.setMethod("PUT");

getUriString

pub fn getUriString(self: Request, uri: []u8) ![]u8

Copy the full URI to the provided buffer. The URI includes scheme and host when returned by Fastly's runtime.

var buf: [4096]u8 = undefined;
const uri = try request.getUriString(&buf);  // "http://example.com/path?query=value"

getUri

pub fn getUri(self: Request, uri_buf: []u8) !std.Uri

Parse the request URI into a std.Uri struct with all components separated.

var buf: [4096]u8 = undefined;
const uri = try request.getUri(&buf);
// uri.scheme - "http" or "https"
// uri.host   - Host component (optional)
// uri.path   - Path component
// uri.query  - Query string (optional)
// uri.port   - Port number (optional)

Example: Protocol-based routing

var uri_buf: [4096]u8 = undefined;
const uri = try downstream.request.getUri(&uri_buf);

// Redirect HTTP to HTTPS
if (std.mem.eql(u8, uri.scheme, "http")) {
    var redirect_buf: [4096]u8 = undefined;
    const https_url = try std.fmt.bufPrint(&redirect_buf, "https://{s}{s}", .{
        uri.host.?.percent_encoded,
        uri.path.percent_encoded,
    });
    try downstream.redirect(301, https_url);
    return;
}

Example: Port-based routing

var uri_buf: [4096]u8 = undefined;
const uri = try downstream.request.getUri(&uri_buf);

// Route admin traffic on port 8443 to admin backend
if (uri.port) |port| {
    if (port == 8443) {
        try downstream.proxy("admin_backend", null);
        return;
    }
}
try downstream.proxy("public_backend", null);

getPath

pub fn getPath(self: Request, uri_buf: []u8) ![]const u8

Extract just the path from the request URI, without query string or fragment. This is the most common operation for routing.

var buf: [4096]u8 = undefined;
const path = try request.getPath(&buf);  // "/api/users"

Example: API gateway routing

var uri_buf: [4096]u8 = undefined;
const path = try downstream.request.getPath(&uri_buf);

if (std.mem.startsWith(u8, path, "/api/users")) {
    try downstream.proxy("users_service", null);
} else if (std.mem.startsWith(u8, path, "/api/orders")) {
    try downstream.proxy("orders_service", null);
} else if (std.mem.startsWith(u8, path, "/static/")) {
    try downstream.proxy("cdn", null);
} else {
    try downstream.response.setStatus(404);
    try downstream.response.body.writeAll("Not found");
    try downstream.response.finish();
}

Example: Blocking sensitive paths

var uri_buf: [4096]u8 = undefined;
const path = try downstream.request.getPath(&uri_buf);

// Block access to admin endpoints from public internet
const blocked_paths = [_][]const u8{ "/admin", "/.env", "/debug", "/metrics" };
for (blocked_paths) |blocked| {
    if (std.mem.startsWith(u8, path, blocked)) {
        try downstream.response.setStatus(403);
        try downstream.response.body.writeAll("Forbidden");
        try downstream.response.finish();
        return;
    }
}
try downstream.proxy("origin", null);

Example: Path rewriting

var uri_buf: [4096]u8 = undefined;
const path = try downstream.request.getPath(&uri_buf);

// Rewrite /v2/api/* to /api/* for the backend
if (std.mem.startsWith(u8, path, "/v2/api/")) {
    const new_path = path[3..];  // Strip "/v2"
    try downstream.request.setUriString(new_path);
}
try downstream.proxy("api_backend", null);

getPathAndQuery

pub fn getPathAndQuery(self: Request, uri_buf: []u8, out_buf: []u8) ![]const u8

Extract the path with query string (but without fragment). Useful when you need to forward the full request path including parameters.

var uri_buf: [4096]u8 = undefined;
var out_buf: [4096]u8 = undefined;
const path_query = try request.getPathAndQuery(&uri_buf, &out_buf);  // "/api/users?id=123"

Example: Logging requests with query parameters

var uri_buf: [4096]u8 = undefined;
var out_buf: [4096]u8 = undefined;
const path_query = try downstream.request.getPathAndQuery(&uri_buf, &out_buf);

var method_buf: [16]u8 = undefined;
const method = try downstream.request.getMethod(&method_buf);

// Log the full request for debugging
std.debug.print("{s} {s}\n", .{ method, path_query });

Example: Cache key generation

var uri_buf: [4096]u8 = undefined;
var out_buf: [4096]u8 = undefined;
const path_query = try downstream.request.getPathAndQuery(&uri_buf, &out_buf);

// Use path + query as cache key for GET requests
if (try downstream.request.isGet()) {
    var entry = cache.lookup(path_query, .{}) catch null;
    if (entry) |*e| {
        defer e.close() catch {};
        const state = try e.getState();
        if (state.isUsable()) {
            // Serve from cache
            var body = try e.getBody(null);
            try downstream.response.setStatus(200);
            // ... stream cached body to response
            return;
        }
    }
}

Example: Forwarding to upstream with original query string

var uri_buf: [4096]u8 = undefined;
var out_buf: [4096]u8 = undefined;
const path_query = try downstream.request.getPathAndQuery(&uri_buf, &out_buf);

// Create a new request to upstream, preserving the original path and query
var upstream_url_buf: [4096]u8 = undefined;
const upstream_url = try std.fmt.bufPrint(&upstream_url_buf, "https://api.internal.example.com{s}", .{path_query});

var req = try Request.new("GET", upstream_url);
var resp = try req.send("internal_api");
try downstream.response.pipe(&resp, true, true);

setUriString

pub fn setUriString(self: Request, uri: []const u8) !void

Set the request URI.

try request.setUriString("/new/path");

parseQueryParams

pub fn parseQueryParams(self: Request, allocator: Allocator) ![]QueryParam

Parse query parameters from the URI.

const params = try request.parseQueryParams(allocator);
for (params) |param| {
    std.debug.print("{s}={s}\n", .{ param.key, param.value });
}

Returns an array of QueryParam structs with key and value fields. Values are URL-decoded.

send

pub fn send(self: *Request, backend: []const u8) !IncomingResponse

Send the request to a backend and get the response.

var response = try request.send("origin");
const status = try response.getStatus();

sendAsync

pub fn sendAsync(self: *Request, backend: []const u8) !PendingRequest

Send the request without waiting for the response, so that several requests can be in flight at the same time. The response is retrieved later from the returned PendingRequest.

var req_a = try Request.new("GET", "https://api.example.com/a");
var req_b = try Request.new("GET", "https://api.example.com/b");
const pending_a = try req_a.sendAsync("api_backend");
const pending_b = try req_b.sendAsync("api_backend");

// Both requests are now in flight; wait for them in any order.
var response_a = try pending_a.wait();
var response_b = try pending_b.wait();

sendAsyncStreaming

pub fn sendAsyncStreaming(self: *Request, backend: []const u8) !PendingRequest

Send the request headers right away, leaving the body open for streaming. Keep writing to request.body and close it to complete the request.

var req = try Request.new("POST", "https://api.example.com/upload");
const pending = try req.sendAsyncStreaming("api_backend");
try req.body.writeAll("first chunk");
try req.body.writeAll("second chunk");
try req.body.close();
var response = try pending.wait();

setCachingPolicy

pub fn setCachingPolicy(self: *Request, policy: CachingPolicy) !void

Set caching behavior for this request.

try request.setCachingPolicy(.{
    .no_cache = false,
    .ttl = 300,              // Cache for 5 minutes
    .serve_stale = 3600,     // Serve stale for 1 hour if origin fails
    .surrogate_key = "my-key",
});

CachingPolicy fields:

FieldTypeDescription
no_cacheboolBypass cache (default: false)
ttl?u32TTL in seconds
serve_stale?u32Stale-while-revalidate time in seconds
pciboolEnable PCI restrictions
surrogate_key[]const u8Surrogate key for purging

setAutoDecompressResponse

pub fn setAutoDecompressResponse(self: *Request, enable: bool) !void

Enable automatic decompression of gzip responses.

try request.setAutoDecompressResponse(true);

logApacheCombined

pub fn logApacheCombined(
    self: Request,
    allocator: Allocator,
    endpoint_name: []const u8,
    status: u16,
    response_size: usize
) !void

Log the request in Apache combined log format.

try request.logApacheCombined(allocator, "access_log", 200, 1234);

close

pub fn close(self: *Request) !void

Close the request prematurely.


PendingRequest

A backend request sent with sendAsync or sendAsyncStreaming, whose response may not have arrived yet.

Methods

wait

pub fn wait(self: PendingRequest) !IncomingResponse

Block until the response arrives, then return it. The pending request is consumed and must not be used afterwards.

var response = try pending.wait();

poll

pub fn poll(self: PendingRequest) !?IncomingResponse

Return the response if it has arrived, or null if it is still pending. Once a response has been returned, the pending request is consumed and must not be used afterwards.

if (try pending.poll()) |response| {
    // The response is ready
} else {
    // Still in flight; do something else in the meantime
}

isReady

pub fn isReady(self: PendingRequest) !bool

Return true if the response has arrived, i.e. if wait would return without blocking. Unlike poll, this does not consume the pending request.

select

pub fn select(pending: []const PendingRequest) !Selected

Block until one of the pending requests completes, and return its response along with its index in the slice. The winning entry is consumed; the others remain pending and can be waited on or selected again.

Selected has two fields: index (usize) and response (IncomingResponse).

Example: fan out to several backends, use whatever answers first

var req_a = try Request.new("GET", "https://mirror-a.example.com/data");
var req_b = try Request.new("GET", "https://mirror-b.example.com/data");
const pending = [_]PendingRequest{
    try req_a.sendAsync("mirror_a"),
    try req_b.sendAsync("mirror_b"),
};
const first = try PendingRequest.select(&pending);
var response = first.response;

For processing every response in completion order, use iterator instead of calling select in a loop.

iterator

pub fn iterator(pending: []PendingRequest) Iterator

Return an iterator that yields the response of each pending request as it arrives, in completion order rather than in the order the requests were sent. The slice is reordered in place as entries are consumed. The iterator's next method returns !?IncomingResponse: the next response to complete, or null once all pending requests have been consumed.

Example: process all responses in completion order

var pending = [_]PendingRequest{ pending_a, pending_b, pending_c };
var responses = PendingRequest.iterator(&pending);
while (try responses.next()) |response| {
    // ... process the response ...
}

RequestHeaders / ResponseHeaders

Header manipulation for requests and responses.

Methods

get

pub fn get(self: Headers, allocator: Allocator, name: []const u8) ![]const u8

Get a header value. Returns FastlyError.FastlyNone if not found.

const content_type = try headers.get(allocator, "Content-Type");

getAll

pub fn getAll(self: Headers, allocator: Allocator, name: []const u8) ![][]const u8

Get all values for a header (for headers that appear multiple times).

const cookies = try headers.getAll(allocator, "Set-Cookie");

names

pub fn names(self: Headers, allocator: Allocator) ![][]const u8

Get all header names.

const header_names = try headers.names(allocator);
for (header_names) |name| {
    std.debug.print("{s}\n", .{name});
}

set

pub fn set(self: *Headers, name: []const u8, value: []const u8) !void

Set a header, replacing any existing value.

try headers.set("X-Custom", "value");

append

pub fn append(self: *Headers, allocator: Allocator, name: []const u8, value: []const u8) !void

Append a value to a header (creates multiple headers with the same name).

try headers.append(allocator, "Set-Cookie", "a=1");
try headers.append(allocator, "Set-Cookie", "b=2");

remove

pub fn remove(self: *Headers, name: []const u8) !void

Remove a header.

try headers.remove("X-Forwarded-For");

Body

HTTP request or response body.

Methods

read

pub fn read(self: *Body, buf: []u8) ![]u8

Read a chunk from the body. Returns an empty slice when complete.

var buf: [4096]u8 = undefined;
while (true) {
    const chunk = try body.read(&buf);
    if (chunk.len == 0) break;
    // Process chunk
}

readAll

pub fn readAll(self: *Body, allocator: Allocator, max_length: usize) ![]u8

Read the entire body. Pass 0 for max_length for no limit.

const data = try body.readAll(allocator, 1024 * 1024);  // Max 1MB
defer allocator.free(data);

write

pub fn write(self: *Body, buf: []const u8) !usize

Write to the body. Returns number of bytes written.

const written = try body.write("Hello, ");

writeAll

pub fn writeAll(self: *Body, buf: []const u8) !void

Write entire buffer to the body.

try body.writeAll("Hello, World!");

append

pub fn append(self: *Body, other: Body) !void

Move the entire content of another body to the end of this one. The bytes are spliced host-side without ever crossing into guest memory, so this is much cheaper than a read/write loop. The other body is consumed.

try response.body.append(upstream_response.body);

close

pub fn close(self: *Body) !void

Close the body.


OutgoingResponse

The response sent back to the client.

Fields

FieldTypeDescription
headersResponseHeadersResponse headers
bodyBodyResponse body

Methods

getStatus

pub fn getStatus(self: OutgoingResponse) !u16

Get the current status code.

setStatus

pub fn setStatus(self: *OutgoingResponse, status: u16) !void

Set the HTTP status code.

try response.setStatus(404);

flush

pub fn flush(self: *OutgoingResponse) !void

Send buffered data without closing. Use for streaming responses.

try response.body.writeAll("First chunk");
try response.flush();
try response.body.writeAll("Second chunk");
try response.finish();

finish

pub fn finish(self: *OutgoingResponse) !void

Send the response and close. Required to send any response.

try response.finish();

pipe

pub fn pipe(
    self: *OutgoingResponse,
    incoming: *IncomingResponse,
    copy_status: bool,
    copy_headers: bool
) !void

Zero-copy an incoming response to the client.

var upstream_resp = try request.send("backend");
try downstream.response.pipe(&upstream_resp, true, true);

IncomingResponse

Response from a backend.

Fields

FieldTypeDescription
headersResponseHeadersResponse headers
bodyBodyResponse body

Methods

getStatus

pub fn getStatus(self: IncomingResponse) !u16

Get the HTTP status code.

close

pub fn close(self: *IncomingResponse) !void

Close the response.


QueryParam

Represents a URL query parameter.

Fields

FieldTypeDescription
key[]const u8Parameter name
value[]const u8Parameter value (URL-decoded)