Skip to content

requests — a browser-impersonating HTTP client ​

A native nimony HTTP client that impersonates real browsers at the byte level — TLS cipher/extension ordering, GREASE, the post-quantum key_share, ALPN/ALPS, HTTP/2 SETTINGS and pseudo-header order, and the exact default header set. It is a C-FFI binding over libcurl-impersonate (the lexiforest fork), driving a single curl_easy_impersonate(target, 1) call to install the whole fingerprint. Standalone: it does not use the rest of the aoughwl net stack, but the impersonation requires the vendored C library. It is a nimony-idiom reimplementation of the Nim2 client under src/requests/ — status-based returns (no exceptions), top-level {.cdecl.}/{.nimcall.} callbacks (no closures), and caller-owned lifetimes.

Status — Run-verified against httpbin.org / example.com (all verbs, 7 profiles, full header/TLS/proxy/cookie/streaming/multipart/concurrency/hooks/retry paths). The three formerly-deferred TODO(nimony) items are now done and live-tested: the INFO_CERTINFO peer certificate-chain walker (certInfoConfig + certChain), file-IO cookie-jar auto-save/load (saveCookies/loadCookies/cookieFile), and cross-thread CURLSH lock callbacks (newThreadSafeShare).

Quickstart ​

nim
import requests

let s = newSession("chrome136")            # falls back to builtins[0] if unknown
let r = s.get("https://httpbin.org/get")
echo r.status, " ", r.ok(), " ", r.contentType()

# full header control — verbatim order, or strip a browser default
discard s.get(url, cfg = orderedHeaders(@[("X-A", "1"), ("X-B", "2")]))
discard s.get(url, cfg = withoutHeaders(@["Accept-Language"]))

# auth + typed bodies
discard s.get(url, @[bearer("token")])
discard s.postForm(url, @[("user", "bob")])
discard s.postJson(url, """{"k":1}""")

# streaming, multipart, upload
discard s.download(url, onChunk, addr ctx)                 # {.nimcall.} sink
discard s.uploadString("PUT", url, bigBody)                # READFUNCTION stream
discard s.postMultipart(url, @[field("u", "bob"),
                               fileField("f", "/a.png")])   # curl owns the boundary

# proxy rotation, coherence audit, retry, concurrency
let pool = newProxyPool(@[proxyEntry("http://p1:8080")])
discard s.get(url, cfg = pool.pick().toConfig())
echo auditSession(s, myHeaders)                            # fingerprint tells
discard s.request("GET", url, retry = retryPolicy(maxAttempts = 3))
let rs = s.getAll(@[u1, u2, u3])                           # curl_multi, order kept
s.close()

request never raises: on a transport failure the returned Response has a non-empty .error and .status == 0.

API ​

Core types ​

symbolsignaturewhat it does
Sessionref object (handle, profile, verifyTls, timeoutMs, followRedirects, maxRedirs, proxy*, cookieFile, share, extra, defaults, retry, hooks)Wraps ONE persistent curl easy handle reused across calls — connection reuse, TLS-session cache and cookie engine all live on it.
Responseobject (status, body, headers, setCookies, effectiveUrl, httpVersion, totalTime, info, error)Result of a transfer. headers keeps order + dups; setCookies are raw Set-Cookie values; non-empty error ⇒ transport failure.
ResponseInfoobject (primaryIp, primaryPort, ttfb, nameLookup, connect, redirectCount, redirectUrl)Connection/transfer metrics pulled off the handle via getinfo.
RequestConfigobject (headerOrder, removeHeaders, proxy, proxyAuth, proxyKind, noProxy, tls, resolve, connectTo, interfaceName, localPort, ipFamily, postRedir, unrestrictedAuth, autoReferer, rawLong, rawStr)Every advanced override a single request can carry. A default value inherits the session/profile — RequestConfig() changes nothing.
TlsConfigobject (cipherList, tls13Ciphers, sslVersionMin/Max, alpn, verifyPeer/Host, caInfo, caPath, clientCert/Key, clientCertType, keyPassword)Opt-in TLS overrides applied on top of the profile.
PreparedRequestobject (meth, url, body, headers)The mutable request a before-hook sees; editing fields changes the wire.
RetryPolicyobject (maxAttempts, baseDelayMs, maxDelayMs, onTransport, on429, on5xx, honorRetryAfter)Opt-in retry/backoff config; maxAttempts <= 1 ⇒ off.
ProxyKindenum pkAuto pkHttp pkHttps pkSocks4 pkSocks4a pkSocks5 pkSocks5hProxy scheme; pkAuto lets curl infer it from the URL.
IpFamilyenum ipAny ipV4 ipV6Address family; ipAny = happy-eyeballs.
Trienum triInherit triOff triOnTri-state whose default leaves the session/profile value.
DataCbproc(chunk: pointer, n: int, userdata: pointer) {.nimcall.}Per-chunk body sink for streaming download.
ReadCbproc(buf: pointer, cap: int, userdata: pointer): int {.nimcall.}Upload source: fill up to cap bytes, return count (0 ⇒ EOF).
BeforeHook / AfterHookproc(prep: ptr PreparedRequest, ud: pointer) / proc(resp: ptr Response, ud: pointer) {.nimcall.}Request/response interceptor proc types.

Sessions & requests ​

symbolsignaturewhat it does
newSessionproc(profile = "chrome136", proxy = "", verifyTls = true, timeoutMs = 30000, followRedirects = true, maxRedirs = 10, proxyAuth = "", cookieFile = "", share: CURLSH = …, retry = RetryPolicy()): SessionCreate an impersonating session. Unknown profile ⇒ builtins[0]. Pass a share to pool state across sessions.
closeproc(s: Session)Clean up the easy handle.
requestproc(s, meth, url, body = "", headers = @[], nobody = false, cfg = RequestConfig(), retry = RetryPolicy()): ResponsePerform a request. Runs before/after hooks, honors retry; never raises.
get / post / put / patch / deleteproc(s, url, [body,] headers = @[], cfg = RequestConfig()): ResponseConvenience verbs over request.
headproc(s, url, headers = @[], cfg = RequestConfig()): ResponseA real HEAD via OPT_NOBODY: status + headers, no body.
optionsproc(s, url, headers = @[], cfg = RequestConfig()): ResponseOPTIONS verb.
retryPolicyproc(maxAttempts = 3, baseDelayMs = 200, maxDelayMs = 20000, onTransport = true, on429 = true, on5xx = true, honorRetryAfter = true): RetryPolicyBuild an opt-in retry policy (exponential backoff, Retry-After wins).
onBeforeRequest / onAfterResponseproc(s: Session, hook, userdata = …)Register a {.nimcall.} interceptor (runs in order; may mutate).
sleepMsproc(ms: int)libc usleep wrapper used by the backoff path.

Response inspection ​

symbolsignaturewhat it does
okproc(r: Response): boolTrue for a 2xx status with no transport error.
headerproc(r: Response, name: string): stringCase-insensitive first-match header ("" if absent).
headerAllproc(r: Response, name: string): seq[string]Every value for name in wire order (multi-value safe).
hasHeaderproc(r: Response, name: string): boolWhether the response carried name.
headerNamesproc(r: Response): seq[string]Header names in server order (dups included).
contentTypeproc(r: Response): stringMedia type sans parameters, lowercased.

Header control ​

symbolsignaturewhat it does
setHeaderproc(s: Session, name, value: string)Add/replace a session-default header (case-insensitive dedup).
appendHeaderproc(s: Session, name, value: string)Append WITHOUT dedup (allows multi-value).
removeHeaderproc(s: Session, name: string)Drop a session-default header.
orderedHeadersproc(pairs: seq[(string, string)]): RequestConfigA config whose headerOrder REPLACES the computed appended set with a verbatim, ordered list — byte-exact control.
withoutHeadersproc(names: seq[string]): RequestConfigA config that strips named curl-default headers (Name: with no value).
mergedHeadersproc(s: Session, headers = @[]): seq[(string, string)]Preview the final appended set: profile.extraHeaders → session.extra → call headers, deduped.
mergeHeadersproc(profile, session, call: seq[(string, string)]): seq[(string, string)]The underlying three-way merge (last wins).

Streaming, upload & multipart ​

symbolsignaturewhat it does
downloadproc(s, url, onData: DataCb, userdata: pointer, headers = @[], cfg = RequestConfig()): ResponseStream the body to onData(chunk, n, ud) instead of buffering; Response has headers/status/timing but empty body.
uploadStreamproc(s, meth, url, read: ReadCb, userdata: pointer, size: int64 = -1, headers = @[], cfg = RequestConfig()): ResponseStream a body from a READFUNCTION; size = -1 ⇒ chunked.
uploadStringproc(s, meth, url, data: string, headers = @[], cfg = RequestConfig()): ResponseConvenience: stream data as the body via READFUNCTION.
fieldproc(name, value: string, contentType = ""): PartA plain multipart text field.
fileFieldproc(name, path: string, filename = "", contentType = ""): PartA file-upload field; curl streams from path, filename defaults to basename.
postMultipartproc(s, url, parts: seq[Part], headers = @[], cfg = RequestConfig()): ResponsePOST a multipart/form-data body via OPT_MIMEPOST (curl owns the boundary).
Partobject (name, filename, contentType, …)One multipart field; build with field / fileField.

Convenience helpers (util) ​

symbolsignaturewhat it does
basicAuthproc(user, password: string): (string, string)Authorization: Basic <base64> header tuple.
bearerproc(token: string): (string, string)Authorization: Bearer <token> header tuple.
encodeUrlproc(s: string): stringPercent-encode (RFC 3986 unreserved kept); local encoder, no std/uri.
encodeFormproc(fields: seq[(string, string)]): stringBuild an application/x-www-form-urlencoded body.
withQueryproc(url: string, params: seq[(string, string)]): stringAppend params to url as a percent-encoded query string.
postFormproc(s, url, fields, headers = @[]): ResponsePOST a urlencoded form (sets Content-Type).
postJsonproc(s, url, body: string, headers = @[]): ResponsePOST a raw JSON string (sets Content-Type: application/json).

TLS & evasion (tls) ​

symbolsignaturewhat it does
insecureTlsproc(): TlsConfigDisable peer + host verification (testing only).
withCAproc(caInfo = "", caPath = ""): TlsConfigTrust a custom CA bundle file and/or directory.
withClientCertproc(cert, key: string, password = "", certType = "PEM"): TlsConfigPresent a client certificate (mutual TLS).
withAlpnproc(on: bool): TlsConfigToggle ALPN explicitly.
customCiphersproc(tls12List: string, tls13List = ""): TlsConfigOverride cipher/ciphersuite lists. Rewrites the ClientHello — breaks JA3/JA4.
pinTlsVersionproc(minVer = 0, maxVer = 0): TlsConfigPin TLS min/max. Pinning MIN breaks the fingerprint.
withTlsproc(cfg: RequestConfig, tls: TlsConfig): RequestConfigAttach a TlsConfig to a RequestConfig (fluent).
tlsConfigproc(tls: TlsConfig): RequestConfigA RequestConfig carrying just this TlsConfig.
auditTlsproc(cfg: RequestConfig): seq[string]Warnings for any TLS override that would break the fingerprint. Empty ⇒ ClientHello still the profile's.

HTTP version & low-level escape hatch ​

symbolsignaturewhat it does
useHttpVersionproc(s: Session, version: int)Pin the negotiated HTTP version via an HTTP_VERSION_* constant.
useHttp3proc(s: Session)Prefer HTTP/3 (QUIC), falling back to h2/1.1 (curl built with ngtcp2).
useHttp3Onlyproc(s: Session)Require HTTP/3 — fail rather than fall back.
setOptionproc(s: Session, opt: CURLoption, value: clong) / (…, value: string)Set any un-wrapped CURLOPT on the handle now.
getInfoStr / getInfoLong / getInfoDoubleproc(s: Session, info: CURLcode): string / int / floatRead any curl getinfo metric off the handle.

Proxies (proxy) ​

symbolsignaturewhat it does
PickStrategyenum ppRoundRobin ppRandomPool rotation strategy (ppRandom uses a local xorshift).
ProxyEntryobject (url, auth, kind)A single proxy: URL + user:password + ProxyKind.
ProxyPoolref object (entries, strategy, idx, rngState)A rotating pool of proxies for a fleet.
proxyEntryproc(url: string, auth = "", kind = pkAuto): ProxyEntryBuild a proxy entry.
newProxyPoolproc(entries = @[], strategy = ppRoundRobin): ProxyPoolCreate a rotating pool.
addproc(pool: ProxyPool, url: string, auth = "", kind = pkAuto)Append a proxy to the pool.
lenproc(pool: ProxyPool): intPool size.
pickproc(pool: ProxyPool): ProxyEntryNext proxy per strategy (empty entry if pool empty).
toConfigproc(e: ProxyEntry): RequestConfigA RequestConfig selecting this proxy (per-request rotation).
setProxyproc(s: Session, e: ProxyEntry)Point a session at this proxy (session-level).
rotateproc(pool: ProxyPool, s: Session): ProxyEntryAdvance the pool and bind the chosen proxy to the session.

Cookies (session engine) ​

symbolsignaturewhat it does
Cookieobject (domain, includeSubdomains, path, secure, httpOnly, expires, name, value)A typed cookie over curl's in-memory engine.
cookiesproc(s: Session): seq[Cookie]Every cookie in the session jar (INFO_COOKIELIST).
cookieproc(s, name: string, domain = ""): stringValue of the first matching cookie ("" if absent).
hasCookieproc(s, name: string, domain = ""): boolWhether a matching cookie exists.
setCookieproc(s, cookie: Cookie) / proc(s, domain, name, value, path = "/", secure = false, httpOnly = false, expires = 0, includeSubdomains = false)Insert/replace a cookie (applied immediately, OPT_COOKIELIST).
clearCookiesproc(s: Session)Erase all cookies (ALL).
clearSessionCookiesproc(s: Session)Drop only session cookies (SESS).
loadCookieLinesproc(s: Session, lines: seq[string])Seed the jar from Netscape cookie-file lines.
dumpCookiesproc(s: Session): stringThe jar as Netscape cookie-file text.
parseNetscapeLine / toNetscapeLineproc(line: string): (Cookie, bool) / proc(c: Cookie): stringNetscape cookie-file line round-trip.
saveCookiesproc(s: Session, path: string): boolFlush the live jar to a Netscape cookie file now (non-raising; false if unopenable).
loadCookiesproc(s: Session, path: string): boolSeed the live jar from a cookie file now (false if unopenable).
cookieFileproc(s: Session, path: string)Bind a file-backed jar: curl reads path at request start (OPT_COOKIEFILE) and rewrites it on close (OPT_COOKIEJAR); existing cookies are loaded immediately.
symbolsignaturewhat it does
CookieJarref object (bound: seq[Session])A programmatic management layer over a session's live engine.
newCookieJarproc(): CookieJarCreate an unattached jar.
attachproc(s: Session, jar: CookieJar)Bind a jar to a session.
isAttachedproc(jar: CookieJar): boolWhether the jar is bound.
listproc(jar: CookieJar, domain = ""): seq[Cookie]Cookies (optionally domain-filtered, suffix match).
getproc(jar: CookieJar, name: string, domain = ""): CookieFirst matching cookie (empty name if absent).
setproc(jar, cookie) / proc(jar, domain, name, value, …)Insert/replace a cookie.
deleteproc(jar: CookieJar, name: string, domain = "")Remove matches (rebuilds the jar without them).
dumpText / seedTextproc(jar: CookieJar): string / proc(jar: CookieJar, text: string)Round-trip the whole jar to/from Netscape text.

Cross-session share (share) ​

symbolsignaturewhat it does
Shareref object (handle: CURLSH)A pool of browser-coherent state for several sessions.
newShareproc(cookies = true, dns = true, tlsSessions = true, connections = true): ShareCreate a CURLSH sharing cookies/DNS/TLS-session/connection cache (single-thread use).
newThreadSafeShareproc(cookies = true, dns = true, tlsSessions = true, connections = true): ShareLike newShare but installs CURLSHOPT_LOCKFUNC/UNLOCKFUNC (top-level {.cdecl.} callbacks backed by a per-curl_lock_data array of std/locks mutexes) so one CURLSH is safe across threads.
closeproc(sh: Share)Tear the share down (after every attached session is closed).

Concurrency (multi) ​

symbolsignaturewhat it does
Requestobject (meth, url, body, headers, cfg, nobody)One request in a concurrent batch.
reqproc(url: string, meth = "GET", body = "", headers = @[], cfg = RequestConfig(), nobody = false): RequestBuild a batch request.
fetchAllproc(s: Session, reqs: seq[Request], maxConcurrent = 8): seq[Response]Run reqs concurrently over curl_multi (order preserved; one failure doesn't sink the batch).
getAllproc(s: Session, urls: seq[string], maxConcurrent = 8): seq[Response]GET a list of URLs concurrently.

TLS certificate chain (certinfo) ​

symbolsignaturewhat it does
CertInfoobject (fields: seq[(string, string)])One certificate in the peer chain, as ordered (key, value) fields exactly as libcurl reports them.
certInfoConfigproc(base = RequestConfig()): RequestConfigA config with OPT_CERTINFO enabled — pass it to the request so certChain can read the result.
certChainproc(s: Session): seq[CertInfo]The peer certificate chain captured on the session's last request (leaf first); walks INFO_CERTINFO (struct curl_certinfo + per-cert curl_slist).
fieldproc(c: CertInfo, key: string): stringFirst field whose key matches key (case-insensitive; "" if absent).
subject / issuerproc(c: CertInfo): stringShortcuts for the Subject / Issuer fields.

Profiles (profiles) ​

symbolsignaturewhat it does
Engineenum eChromium eFirefox eSafariBrowser engine family.
Profileobject (name, target, engine, version, os, released, extraHeaders)An impersonation cohort as data; target is the curl-impersonate token.
builtinsconst array[7, Profile]The 7 profiles: chrome136, chrome131, chrome131_android, edge101, firefox135, safari18_4, safari18_4_ios.
findProfileproc(name: string): (bool, Profile)Look up a profile by name (found flag + value).
getproc(name: string): ProfileLook up by name; default (empty .name) on miss.
profileNamesproc(): stringComma-joined built-in names (diagnostics).
acceptEncodingproc(p: Profile): stringThe cohort's exact Accept-Encoding.
epochDayOfproc(iso: string): intParse yyyy-MM-dd to days-since-epoch (-1 on bad input).
ageDaysproc(p: Profile, asOfEpochDay: int): intDays between release and asOf.
staleproc(p: Profile, asOfEpochDay: int, maxAgeDays = 120): boolWhether the cohort is older than maxAgeDays.
freshnessNoteproc(p: Profile, asOfEpochDay: int): stringHuman-readable freshness/staleness warning.

Coherence audit (coherence) ​

symbolsignaturewhat it does
Warningtype = stringA single coherence finding.
auditproc(p: Profile, headers: seq[(string, string)], proxyGeoLang = ""): seq[Warning]Lint headers against a profile: dup/managed/botty headers, UA-vs-engine, platform-vs-OS, Accept-Language-vs-geo, Firefox Sec-CH-UA. Empty ⇒ coherent.
auditSessionproc(s: Session, headers = @[], proxyGeoLang = ""): seq[Warning]Audit everything a session would send against its active profile.

FFI (ffi) ​

The full libcurl-impersonate binding, re-exported by the umbrella. Opaque handle types CURL, CURLM, CURLSH, curl_mime, curl_mimepart; code types CURLcode/CURLMcode/CURLSHcode; option enums CURLoption/CURLMoption/CURLSHoption; and the curl_slist/CurlSlistNode/CurlCertInfo/CURLMsg structs. Constant families: OPT_*, INFO_*, PROXYTYPE_* (0/2/4/5/6/7 = http/https/socks4/5/4a/5h), AUTH_*, SSLVERSION_*, HTTP_VERSION_* (_1_0/_1_1/_2_0/_2TLS/_3/_3ONLY), LOCK_DATA_*, SHOPT_*.

symbolsignaturewhat it does
curl_easy_init / _cleanup / _reset / _performover CURLHandle lifecycle + synchronous transfer.
curl_easy_setopt / _getinfo{.varargs.} over CURLSet an option / read an info metric.
curl_easy_impersonateproc(handle: CURL, target: cstring, defaultHeaders: cint): CURLcodeInstall a browser's whole TLS+HTTP/2 fingerprint.
curl_easy_strerror / errStr / curlOkcode → string / boolError text and OK test.
curl_slist_append / _free_allover nil ptr curl_slistBuild/free a header list.
curl_mime_*init/free/addpart/name/data/filedata/filename/typeMultipart MIME construction.
curl_multi_*init/cleanup/add_handle/remove_handle/perform/poll/info_read/setoptThe concurrent transfer interface.
curl_share_*init/cleanup/setopt/strerrorCross-session shared state.
curl_global_initproc(flags: clong): CURLcodeOne-time global init (driven by the client).
cstrToStringproc(cs: cstring): stringWalk a NUL-terminated C string into a nimony string (no $(cstring) in nimony).

Design notes ​

  • The fingerprint is installed inside the library. A single curl_easy_impersonate(target, 1) call sets the exact TLS ordering, GREASE, key_share, ALPN/ALPS, HTTP/2 SETTINGS, pseudo-header order and default header set. Everything you layer on (headers, TLS knobs, HTTP version) risks breaking that coherence — hence auditTls and the coherence linter, which name the exact tells.
  • No exceptions. Transport failures surface as Response.error (status 0); lookups return default/empty values, with a found-flag where the distinction matters. This is the aoughwl idiom throughout the stack.
  • Callbacks are top-level {.nimcall.}/{.cdecl.} procs, not closures, always paired with an explicit userdata: pointer you cast back inside. The pointed-at object must outlive the synchronous perform — the streaming/upload/multi paths pre-size their sink seqs so element addresses stay valid.
  • Header precedence (appended set, lowest→highest): profile.extraHeaders → session.extra → per-call headers, deduped last-wins — unless orderedHeaders replaces the whole computed set with a verbatim list.
  • One handle per session (reuse = browser-like connection/TLS/cookie behavior); the concurrency path uses its own easy handles under curl_multi on a single thread — genuine I/O concurrency without OS threads or shared-handle hazards.
  • nimony gotchas, learned porting: nilable pointers/refs need an explicit nil ptr T/nil pointer qualifier (plain forms are non-nil); toCString only on var string locals; POST bodies use OPT_COPYPOSTFIELDS so curl owns the copy; ASCII helpers char-walk (strutils slice ops are .raises); no std/random (local xorshift) or std/uri (local percent-encoder).

Requirements ​

  • nimony toolchain (aoughwl aowl / nimony). Umbrella import requests re-exports every module: ffi, profiles, client, util, headers, tls, proxy, coherence, cookies, cookiejar, certinfo, share, multi.
  • libcurl-impersonate (the lexiforest curl-impersonate fork, built with ngtcp2 for HTTP/3) — the impersonation is not optional; the browser fingerprint lives in this C library. Vendored under vendor/curl-impersonate/lib. Link it and set an rpath at build time (nimony has no compile-time rpath block):
    nimony c -r \
      --passl:-L.../vendor/curl-impersonate/lib \
      --passl:-Wl,-rpath,.../vendor/curl-impersonate/lib \
      --path:.../requests/nimony \
      yourprog.nim
  • Origin: a nimony-native reimplementation of the Nim2 client under src/requests/, over the same libcurl-impersonate FFI. No dependency on the rest of the aoughwl net stack.

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.