# Architecture of Lightpanda's HTTP Client Using libcurl: Core Components and Data Flow

> Explore the architecture of Lightpanda's HTTP client, a type-safe Zig wrapper around libcurl. Understand its core components and data flow for efficient network requests.

- Repository: [Lightpanda/browser](https://github.com/lightpanda-io/browser)
- Tags: architecture
- Published: 2026-03-14

---

**Lightpanda's browser implements its HTTP client as a type-safe Zig wrapper around libcurl, encapsulating native CURL handles in `src/sys/libcurl.zig` with ergonomic request/response abstractions and deterministic resource management.**

The Lightpanda browser engine handles all network operations through a unified HTTP client built on **libcurl**. This architecture leverages Zig's foreign-function interface (FFI) to wrap the low-level C API into a safe, ergonomic module located in `src/sys/libcurl.zig`. The design prioritizes **zero-copy** data handling and explicit memory management while providing high-level convenience methods for the page loader and resource fetchers.

## Core Components

The networking layer consists of four primary building blocks that bridge Zig's safety guarantees with libcurl's performance.

### CurlHandle Lifecycle Management

The **CurlHandle** struct encapsulates a raw `CURL *` pointer from libcurl's C API. Defined in `src/sys/libcurl.zig`, this component manages the entire lifecycle of a connection—from `curl_easy_init` through `curl_easy_cleanup`. It stores per-request state including headers, body buffers, and callback contexts, ensuring that native resources are released through Zig's `deinit` pattern even when errors occur.

### HttpClient High-Level Facade

The **HttpClient** provides the primary interface for the rest of the browser engine. Exposing methods like `request`, `get`, and `post`, this facade creates a `CurlHandle` for each operation, configures common options (`CURLOPT_URL`, `CURLOPT_HTTPGET`, `CURLOPT_POSTFIELDS`), and orchestrates execution. According to the source code in `src/sys/libcurl.zig`, the client handles time-out configuration, redirect following, and header injection automatically.

### Callback Architecture

Zig functions registered as libcurl callbacks—`write_cb`, `header_cb`, and `read_cb`—funnel streaming data into Zig-managed buffers. These callbacks write directly into a dynamically growing `std.ArrayList(u8)` for response bodies and `std.HashMap` structures for headers. This approach avoids intermediate copies, allowing the browser to stream large resources efficiently while maintaining type safety.

### Error Code Translation

Rather than propagating raw libcurl integer codes (`CURLE_COULDNT_CONNECT`, `CURLE_WRITE_ERROR`), the wrapper maps all `CURLE_…` constants into native Zig `Error!` values. This integration allows calling code throughout `src/page/loader.zig` to use standard `try`/`catch` error handling idioms instead of manual error code inspection.

## Data Flow Architecture

Understanding how a request moves through the system reveals the architectural decisions behind resource efficiency.

### Request Initialization

A consumer initiates communication by calling `HttpClient.request(method, url, opts)` or convenience helpers like `HttpClient.get`. The client allocates a new `CurlHandle`, attaches the Zig callback functions, and translates method parameters into libcurl option calls. Configuration includes setting `CURLOPT_URL` for the endpoint and `CURLOPT_HTTPHEADER` for any custom headers.

### Execution and Data Streaming

The handle invokes `curl_easy_perform` to begin the network operation. As libcurl receives data from the wire, it triggers the registered callbacks continuously. The **write callback** appends bytes directly to the pre-allocated Zig buffer, while the **header callback** populates the response header map. This streaming approach means the browser begins processing data before the entire payload arrives, reducing latency for large assets.

### Response Assembly and Cleanup

Once `curl_easy_perform` returns, the wrapper extracts the HTTP status code via `curl_easy_getinfo` and assembles a `Response` struct containing status, headers, and body slices. The `CurlHandle.deinit` method then calls `curl_easy_cleanup` unconditionally, ensuring no native file descriptors or memory leak even if the request failed. The assembled `Response` returns as `!Response` to the caller in `src/page/loader.zig` or other resource loaders.

## Key Architectural Benefits

**Zero-Copy Buffering**: By writing callback data directly into `std.ArrayList(u8)` without intermediate C buffers, the architecture eliminates unnecessary memory copies during resource fetching.

**Deterministic Resource Management**: Zig's explicit `defer` and `errdefer` patterns guarantee that `curl_easy_cleanup` executes regardless of success or failure paths, preventing the handle leaks common in manual C resource management.

**Extensible Configuration**: Adding new libcurl features—such as proxy support, certificate verification, or HTTP/2—requires only extending the `CurlHandle` configuration section in `src/sys/libcurl.zig` without modifying higher-level client code.

**Uniform Error Handling**: The translation layer allows the entire browser engine to treat network failures as standard Zig errors, enabling consistent recovery strategies across page loading, script fetching, and image decoding.

## Implementation Examples

The following patterns demonstrate how the architecture is consumed within the browser engine.

### Simple GET Request

This example from the source tree fetches a URL and prints the response metadata:

```zig
const std = @import("std");
const libcurl = @import("sys.libcurl");

pub fn fetchUrl(allocator: *std.mem.Allocator, url: []const u8) !void {
    var client = try libcurl.HttpClient.init(allocator);
    defer client.deinit();

    const response = try client.get(url, .{});
    std.debug.print("Status: {}\n", .{response.status});
    std.debug.print("Body ({d} bytes):\n{*s}\n", .{
        response.body.len,
        response.body,
    });
}

```

### POST with JSON Payload

Posting structured data requires only configuring the `RequestOptions` struct with headers and body content:

```zig
pub fn postJson(allocator: *std.mem.Allocator, url: []const u8, json: []const u8) !void {
    var client = try libcurl.HttpClient.init(allocator);
    defer client.deinit();

    const opts = libcurl.RequestOptions{
        .headers = &[_][]const u8{
            "Content-Type: application/json",
        },
        .body = json,
    };

    const response = try client.post(url, opts);
    std.debug.print("POST returned {d}\n", .{response.status});
}

```

## Summary

- **Primary wrapper**: All libcurl integration lives in `src/sys/libcurl.zig`, providing a type-safe bridge between C and Zig.
- **Handle management**: The `CurlHandle` struct owns the native `CURL *` pointer and guarantees cleanup through Zig's `deinit` pattern.
- **Streaming architecture**: Callbacks write directly into Zig buffers during `curl_easy_perform`, enabling zero-copy reception of responses.
- **Error integration**: Libcurl's `CURLE_…` codes map to Zig `Error!` types, allowing standard error propagation throughout the browser.
- **Consumer locations**: The page loader in `src/page/loader.zig` uses this client to fetch HTML, CSS, JavaScript, and subresources.

## Frequently Asked Questions

### How does Lightpanda manage native libcurl resources in Zig?

The browser uses the **CurlHandle** struct in `src/sys/libcurl.zig` to encapsulate the native `CURL *` pointer. This handle implements a `deinit` method that calls `curl_easy_cleanup`, which is invoked via `defer` or `errdefer` in calling code. This pattern ensures that native memory and file descriptors are released even if network errors or parsing failures occur during request execution.

### What makes Lightpanda's HTTP client implementation zero-copy?

The architecture registers Zig callbacks (`write_cb`, `header_cb`) directly with libcurl via `CURLOPT_WRITEFUNCTION` and `CURLOPT_HEADERFUNCTION`. These callbacks append incoming bytes to pre-allocated Zig structures like `std.ArrayList(u8)` without intermediate C buffers or string conversions. This allows response data to stream directly into the memory space where the browser will process it, eliminating redundant copy operations.

### How are libcurl errors exposed to the rest of the browser engine?

The wrapper translates libcurl's integer error codes—such as `CURLE_COULDNT_CONNECT` or `CURLE_WRITE_ERROR`—into Zig error unions (`Error!Response`). This translation happens immediately after `curl_easy_perform` returns in `src/sys/libcurl.zig`. Consequently, components like the page loader in `src/page/loader.zig` can use standard `try` statements to propagate network failures, treating HTTP errors identically to memory allocation or parsing errors.

### Which browser components consume the HTTP client?

The primary consumer is the **page loader** located in `src/page/loader.zig`, which uses the client to fetch HTML documents, external stylesheets, JavaScript modules, and image resources. Additional consumers include any subsystem requiring network access, such as WebSocket implementations or diagnostic tools, all importing the `HttpClient` from `src/sys/libcurl.zig` rather than interfacing with libcurl directly.