Open-Source Wikis

/

curl

/

Background

/

Design decisions

curl/curl

Design decisions

A handful of design choices that have shaped curl over multiple decades. Each item summarises the choice, names the alternatives that lost, and points at where the current implementation lives.

C89 as the floor

curl targets C89 with the explicit goal of running on every machine that has a C compiler and 64-bit integer support. This costs convenience features (no inline, no compound literals, no flexible array members) but buys curl a place on AIX, Solaris, BeOS-derived systems, OS/400, and a list of platforms most projects do not even know exist.

The rule is enforced by scripts/checksrc.pl and by CI matrices that build with old toolchains. The portability layer in curlx papers over the differences.

One library, many TLS backends

Most network libraries link against one TLS implementation. curl supports OpenSSL (and forks), GnuTLS, mbedTLS, Schannel, wolfSSL, Rustls, and Apple SecTrust simultaneously. The cost is the Curl_ssl vtable in lib/vtls/vtls.h and one large adapter file per backend. The benefit is that distributors and embedders ship curl with whichever TLS library their platform already provides.

Backends that fail this maintenance test get removed. The 2025 cleanup dropped BearSSL, SecureTransport, and msh3.

The multi state machine is the only loop

Around 2013 the project unified two parallel I/O implementations (curl_easy_perform had its own loop, curl_multi_perform had another) into one. Today both entry points feed Curl_multi_runsingle(). The win was eliminating duplicated bug surfaces; the cost was that every protocol handler had to fit into the multi state machine.

This decision is what made HTTP/2 multiplexing, HTTP/3, and curl_multi_socket_action possible without rewrites of each protocol.

Connection filters over inheritance

The cfilter framework (introduced 2022, matured 2023) replaced a sea of if(conn->ssl_used) and if(conn->use_proxy) branches with a stack of small filters that each do one thing. The win is composability — HTTPS-over-HTTPS-proxy, TLS-on-TLS, HTTP/2-CONNECT all just become "stack the right filters". The cost is one indirection per byte.

See lib/cfilters.c, lib/cfilters.h, and Connection filters. The decision was incremental: HTTP/2 multiplexing led to filters, then HTTP/3 led to deeper layering, then proxy work generalised the abstraction.

Public ABI is sacred

SONAME 4 has held since 2006. The codebase prefers introducing a new option over changing the meaning of an existing one. Public structs are opaque (CURL, CURLM, CURLU, CURLSH, CURLM, CURLM); applications use accessor functions only.

This makes adding features slow and rewards thinking about the option name and its long-term meaning. It also means upgrading libcurl on a 20-year-old binary just works.

Hand-written parsers, fed through fuzzers

Where many projects would reach for a parser-combinator library, curl uses hand-written parsers in lib/curlx/strparse.c, lib/cookie.c, lib/altsvc.c, etc. The parsers are deliberately non-allocating where possible and length-bounded everywhere. OSS-Fuzz has been running them since 2017.

The trade-off: less expressive parser code, but fewer dependencies and easier static reasoning about memory safety.

docs as part of the source

Every command-line option lives in docs/cmdline-opts/<option>.md and every libcurl option in docs/libcurl/opts/CURLOPT_*.md. The man pages are generated from these markdown sources by scripts/managen and scripts/cd2nroff. CI fails if a new option exists without a curldown file (or vice versa).

The 2024 migration to "curldown" replaced hand-edited nroff with a strict markdown subset, eliminating a long-running source of doc rot.

Tests are number-keyed and many

Almost 2,000 numbered files under tests/data/test* is unusual. The numbering is dense, not sparse — a new test takes the next free number. The rationale is that tests are referred to by number in commit messages, in RELEASE-NOTES, and in CI logs; numbers are stable, names are not. The downside is that the structure of the suite is not visible from the file names; the test framework parses keywords from each file to compose the index.

Sharing is opt-in, not default

Caches (DNS, connection, cookies, HSTS, alt-svc, TLS sessions) belong to the multi handle by default. Sharing them across handles requires a CURLSH * and explicit lock callbacks (CURLSHOPT_LOCKFUNC/CURLSHOPT_UNLOCKFUNC). This puts thread-safety responsibility on the application, but keeps libcurl free of internal locking — which has historically been a source of subtle bugs.

No git push-protection rebases on master

master is linear. Maintainers either rebase-merge or fast-forward; merge commits are vanishingly rare. This makes git bisect a precision tool: every commit is a buildable, testable revision.

Two build systems, kept in sync

Autotools (configure.ac + Makefile.am) and CMake (CMakeLists.txt) coexist. The configure-vs-cmake.yml workflow builds the same target with both and diffs the result. The argument for keeping both: each is required by a substantial part of the user base. The argument against: doubled build maintenance. The argument-for has won every time.

Why this list is short

curl deliberately makes few sweeping decisions. Most of the codebase is a sediment of many small, local choices defended by the test suite. New features that require sweeping changes get pushed back, broken into smaller pieces, or rejected. The "feature window" rule (docs/CONTRIBUTE.md) — only bug fixes merge in the 10 days after a release — exists to prevent decision-by-rebase.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Design decisions – curl wiki | Factory