How Ladybird's Node Client Interacts with the RequestServer: IPC Architecture Explained

Ladybird's Node client delegates all network I/O to a dedicated RequestServer process via SerenityOS-style IPC, translating JavaScript fetch() calls into start_request messages and resolving Promises when request_complete callbacks return.

The Ladybird browser implements a strict separation between its JavaScript runtime and network stack. Instead of performing socket operations directly, the Node client acts as a thin wrapper that marshals request data across a Unix socket to the RequestServer. This architecture keeps the rendering process lightweight while centralizing TLS, caching, and protocol handling in a privileged service.

Establishing the IPC Connection

When the browser process launches, it initializes the RequestServer service and creates a communication channel for the Node client. On desktop platforms, the server binary starts directly; on Android, the system uses UI/Android/src/main/cpp/RequestServerService.cpp to adopt a passed file descriptor and construct the transport layer.

auto socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
auto client = TRY(RequestServer::ConnectionFromClient::try_create(make<IPC::Transport>(move(socket))));

The Node side constructs an IPC::Connection to the RequestClientEndpoint, defined in Services/RequestServer/RequestClient.ipc. This endpoint exposes the API that the JavaScript runtime invokes for all network operations, including HTTP requests, WebSocket connections, and DNS resolution.

Initiating Requests via start_request

When JavaScript code calls fetch(url, options), the binding layer creates a Request object and invokes ConnectionFromClient::start_request. This method serializes the fetch parameters into an IPC message sent to the RequestServer.

virtual void start_request(u64 request_id,
                           ByteString method,
                           URL::URL url,
                           Vector<HTTP::Header> headers,
                           ByteBuffer body,
                           HTTP::CacheMode cache_mode,
                           HTTP::Cookie::IncludeCredentials include_credentials,
                           Core::ProxyData proxy_data) override;

The request ID parameter enables the client to correlate asynchronous responses with their original requests. The Node client passes method, url, headers, body, and cache settings exactly as specified in the JavaScript options object.

Processing Requests with libcurl

Inside Services/RequestServer/ConnectionFromClient.cpp, the server stores the request in m_active_requests and instantiates a Request object. The Request class, implemented in Services/RequestServer/Request.cpp, wraps libcurl to perform the actual network operation.

The implementation configures TLS using the default certificate path (RequestServer::g_default_certificate_path), sets up a timer (m_timer) for timeouts, and registers libcurl callbacks (on_socket_callback, on_timeout_callback) to drive asynchronous I/O without blocking the main thread.

Receiving Responses Through request_complete

When libcurl finishes the transfer, the Request object calls back into ConnectionFromClient::request_complete, which sends an IPC message to the Node client:

void request_complete(Badge<Request>, Request const&);

The client receives a Messages::RequestServer::RequestComplete message (generated from RequestClientEndpoint.idl) and resolves the JavaScript Promise with a Response object containing the status code, headers, and response body. This asynchronous callback mechanism ensures the JavaScript runtime remains responsive during network latency.

Advanced Network Features

Beyond standard HTTP requests, the Node client interacts with the RequestServer for specialized network operations:

  • WebSocket Support: The client calls websocket_connect, websocket_send, and websocket_close, implemented in WebSocketImplCurl.cpp, to handle bidirectional communication over libcurl.
  • DNS Customization: Methods like set_dns_server and set_use_system_dns allow the JavaScript runtime to specify custom resolvers when parsing URLs or making requests.
  • Cache Management: The server exposes set_disk_cache_settings and remove_cache_entries_accessed_since to control HTTP caching behavior.
  • Revalidation: For conditional requests, the server uses start_revalidation_request to handle If-None-Match headers and returns 304 status codes via the standard request_complete path.

Practical Example: Tracing a Fetch Request

Consider a standard fetch() call in a Ladybird-hosted script:

fetch("https://example.com/api", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ hello: "world" })
})
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.error(err));

This JavaScript triggers the following interaction between the Node client and RequestServer:

  1. The JS binding invokes ConnectionFromClient::start_request with request_id=1, method="POST", and the JSON payload.
  2. The RequestServer creates a Request instance, configures libcurl with TLS settings, and schedules the I/O.
  3. Upon completion, the server calls request_complete with the HTTP response data.
  4. The Node client converts the IPC message into a Response object and resolves the original Promise.

Summary

  • Ladybird's Node client does not perform socket operations directly; it marshals requests to a separate RequestServer process.
  • Communication uses SerenityOS-style IPC over Unix sockets, with endpoints defined in RequestClient.ipc.
  • The start_request method initiates HTTP requests, while request_complete delivers asynchronous responses.
  • The RequestServer uses libcurl for transport, handling TLS, redirects, cookies, and caching internally.
  • This architecture isolates network complexity from the rendering engine, enabling reuse across desktop and Android platforms.

Frequently Asked Questions

Why does Ladybird use a separate RequestServer process instead of handling HTTP directly in the Node client?

The separation enhances security and stability by isolating network operations—including TLS certificate validation, DNS resolution, and untrusted server data—from the JavaScript runtime. According to the Ladybird source code, this design prevents libcurl and its dependencies from running inside the rendering process, reducing the attack surface and allowing the network stack to be shared across multiple browser instances.

How does the Node client handle WebSocket connections?

WebSocket support follows the same IPC pattern as HTTP requests. The JavaScript runtime calls websocket_connect on the ConnectionFromClient, which the RequestServer handles using WebSocketImplCurl. Data flows bidirectionally through IPC messages: the client sends frames via websocket_send, and the server pushes received frames back through dedicated IPC callbacks, maintaining the abstraction that the Node client remains a thin wrapper.

What happens if the RequestServer process crashes during an active request?

Since the Node client maintains the IPC::Connection, a server crash would disconnect the socket, causing pending start_request calls to fail with connection errors. The JavaScript bindings would reject the associated Promises with network errors. The browser process monitors the RequestServer health and can restart the service, though in-flight requests would need to be retried by the application code.

How are DNS settings customized in this architecture?

The Node client exposes set_dns_server and set_use_system_dns methods that map to IPC calls handled by ConnectionFromClient. When JavaScript code requires custom name resolution—such as when constructing a URL object or initiating a fetch with specific DNS requirements—these methods configure the libcurl resolver inside the RequestServer before the actual HTTP connection begins.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →