Skip to content
Haiku OSDeep Dive Published Updated 9 min readViews unavailable

Haiku's Private BUrlRequest: Protocol Dispatch, Worker Lifetime, and Callback Boundaries

Trace Haiku's private URL-request factory, worker lifecycle, context sharing, and listener dispatch without mistaking completion for HTTP success.

Haiku’s URL stack is easier to reason about when its layers stay separate: BUrl represents and parses a URL, BUrlProtocolRoster selects an implementation, BUrlRequest owns a protocol worker lifecycle, and a listener reports progress and completion. The request path has useful lifecycle hooks, but the implementation discussed here lives under Haiku’s headers/private/netservices tree and in the BPrivate::Network namespace. This is a compatibility warning inferred from the private header location and namespace, not a separate formal ABI guarantee. The details below are pinned to Haiku revision 5e095f4c75c3d1cd86bb25831b030a826fb8fef8; check the exact source shipped with the target system before depending on them.

The factory is a protocol allowlist, not an extensible plug-in promise

At that revision, BUrlProtocolRoster::MakeRequest() selects request implementations by the parsed protocol string. Its branches construct HTTP, HTTPS, file, data, and Gopher requests; an unrecognized scheme returns NULL. The implementation also contains a TODO about creating the protocol implementation through an add-on interface. That is a statement about this source revision, not a prediction about a future Haiku release.

The distinction prevents a common design mistake: parser acceptance is not evidence that the request factory can service a URL. BUrl handles syntax and component boundaries; the roster’s protocol switch is a separate capability decision. A URL can be syntactically valid while the current request factory has no implementation for its scheme. Check the factory result and provide a useful unsupported-protocol path rather than assuming that every custom scheme is dynamically discoverable.

using namespace BPrivate::Network;

BUrlRequest* request = BUrlProtocolRoster::MakeRequest(
	url, output, listener, profileContext);
if (request == nullptr) {
	// The factory uses NULL for an unsupported protocol or allocation failure;
	// this interface does not distinguish the two cases.
	return B_ERROR;
}

thread_id worker = request->Run();
if (worker < B_OK) {
	// Reconcile request state before releasing any objects it may reference.
	return worker;
}

This is an interface sketch for the private headers at the pinned revision, not portable application code or a complete ownership wrapper. A real caller must adopt the returned raw request into a longer-lived operation owner before this scope exits, and retain the output and listener for the asynchronous operation. The implementation stores output and listener as raw pointers, while it holds the context through a BReference. Keep the output and listener alive until the request has conclusively finished; the request’s reference keeps its context alive. Do not infer ownership transfer for the stream or listener from the factory call.

A context is security and session state, not just an options bag

BUrlRequest replaces a null context with a static default BUrlContext reference in its constructor. BUrlContext contains a cookie jar, URL-associated authentication, proxy settings, and certificate exceptions. As a result, requests that take the default path use the same context object in this implementation. That can be convenient for a deliberately shared session; it is a poor implicit boundary between unrelated accounts, profiles, or trust policies.

Create and retain an explicit context when the application needs isolation. Decide intentionally whether cookies, proxy configuration, authentication entries, or certificate exceptions should cross a boundary. A certificate exception is not a generic way to make TLS succeed: do not add a broad exception merely to suppress a verification error, and do not reuse a context across users unless shared identity state is the intended behavior. These are consequences of the fields exposed by the private context header; they do not imply that every protocol implementation consumes every field identically.

The same source-versus-contract distinction matters for versions. BUrlContext and BUrlRequest are private APIs; an application that depends on their layout or exact behavior should isolate them behind its own adapter, compile against the installed headers, and retest after Haiku updates.

Run() and Stop() describe a worker, not a synchronous transaction

The pinned BUrlRequest implementation starts a named kernel thread with spawn_thread() and resume_thread(). A successful Run() therefore means that a worker was launched; it does not mean DNS, TLS, protocol negotiation, or body transfer has completed. If creation or resumption fails, inspect the returned value and the request’s state rather than proceeding as if a result were available.

There is a sharper failure edge when resumption fails after thread creation: this revision has already set fRunning = true, then returns resume_thread()’s error without resetting fRunning or assigning that error to fThreadStatus. Since the thread entry point sets fThreadStatus only after it starts, IsRunning() and Status() are not by themselves proof that the worker completed or that the request can be immediately destroyed on this path. Treat this as a source-level defect/edge in the pinned implementation; a caller must account for the target system’s thread state and preserve request-owned state until cleanup is demonstrably safe.

Stop() in that base implementation sets the request’s quit flag when fRunning is true. It does not call a general thread-kill primitive, join the worker, or itself prove that the protocol loop has exited. The concrete protocol loop must cooperate with that flag. Calling Stop() after the request is no longer running returns B_ERROR in this revision. The base destructor calls Stop(), but does not wait for the worker to finish. Consequently, treat stop as a cancellation request and keep the request, listener, and output stream valid until the completion path has been reconciled; the request holds its own reference to the context. A UI can mark an operation as cancelling immediately, but should not free its backing objects solely because Stop() returned B_OK.

An owner can model the operation as created → running → stop requested → completion observed → released. Record the worker identifier returned by Run(), retain the request with the operation state, and make one owner responsible for final cleanup. On launch failure, do not treat IsRunning() or Status() as a join operation; the resumption-failure edge above requires release to be based on confirmed thread/request cleanup, not just a status read. Avoid concurrent mutation of URL, context, listener, or output while the request is active: the setters in this implementation reject changes when IsRunning() is true, which is a useful guard but not a substitute for a caller-owned state machine.

Completion reports the protocol loop result, not an HTTP status class

The base thread entry point runs protocol setup and _ProtocolLoop(), stores that loop’s status_t, clears fRunning, and then invokes RequestCompleted(request, status == B_OK) when a listener exists. The callback’s Boolean is therefore derived from the protocol-loop status, not a general HTTP status-class test. In the reviewed BHttpRequest::_ProtocolLoop() implementation, a completed 3xx response with redirect-following disabled can fall through to B_OK, while status 404 is explicitly mapped to B_RESOURCE_NOT_FOUND. Inspect the HTTP result’s status code separately; the completion Boolean also says nothing about whether the body matches the application’s expected schema or whether the resulting file was committed safely.

Keep the dimensions distinct in the application result model:

  1. Worker/protocol-loop status: did the implementation finish with B_OK?
  2. HTTP response state: what status code and headers did the HTTP implementation report?
  3. Body integrity: were the expected number of bytes received and validated?
  4. Application acceptance: did parsing, signature verification, and any atomic file replacement succeed?

The private base BUrlResult only declares generic content type and length accessors; protocol-specific results provide further details. Do not cast a generic result to an HTTP-specific result unless the factory actually created an HTTP request. Preserve partial output separately and publish it only after the checks appropriate to the application have passed.

Listener dispatch crosses a thread boundary, with one synchronous exception

The listener interface includes connection, hostname, response, headers, byte-progress, completion, debug, and certificate-verification callbacks. A direct listener should not assume that it runs on the application’s window thread. The base completion callback is invoked from the request worker. Use a message handoff for UI state, and never touch a BView or another looper-owned object from the network worker merely because a progress callback arrived.

Haiku’s private BUrlProtocolDispatchingListener sends ordinary notifications through a BMessenger. Its certificate-verification callback is different: the implementation sends a message with a synchronous reply and returns the reply’s continue/abort Boolean. That means the request path may be waiting for the certificate decision while the target handler processes it. Keep that handler responsive, avoid asking it to synchronously wait for the same request to advance, and fail closed if policy cannot make a defensible decision. The asynchronous listener wrapper uses this dispatching listener for its transparent mode; “asynchronous” does not mean every callback can be deferred without a reply.

There is a hostname-notification defect in this reviewed revision’s transparent wrapper path: BUrlProtocolDispatchingListener::HostnameResolved() writes the address under url:hostIp, but BUrlProtocolAsynchronousListener::MessageReceived() looks up url:ip and ignores the lookup result before forwarding ip. At this pinned revision, BMessage::FindData() initializes the output pointer to NULL before attempting the lookup, and FindString() delegates to that lookup; with the mismatched key, this call therefore forwards NULL, not an indeterminate pointer. The resolved address is still lost, and any downstream failure depends on how the recipient handles a null hostname. Treat this as a defect of the pinned revision/path, not a general listener contract; verify a fix or handle hostname notifications defensively before relying on that wrapper path.

When you consume these notifications, copy the fields you need into application-owned state rather than treating callback pointers as durable handles. Use progress events for presentation, not as proof of final success. On completion, stop updating the UI, inspect the appropriate protocol result, validate the body, and only then mark the operation complete.

Keep the abstraction replaceable

The private request stack is instructive because it exposes real systems concerns: protocol dispatch, streaming output, shared session state, worker cancellation, callback routing, and trust decisions. It is also an implementation detail. Put it behind a narrow application interface such as StartDownload, RequestStop, and a value-type completion result. That adapter can encode the target release’s lifetime rules, map private statuses into application-owned errors, isolate cookie and certificate policy, and be replaced if a later Haiku revision changes the private network-services classes.

The neighboring Network Kit HTTP client guide covers higher-level HTTP concerns such as redirects, cookies, and TLS boundaries. This source-level tour is narrower: it explains why a factory, worker, callback, and context should not be collapsed into one Boolean called “download succeeded.”

Related:

Sources:

Comments