requests
An HTTP/1.1 client in the spirit of Python's requests — and the first standard-library module written in pure cheatah (requests.purr, compiled by purrc into an importable module). All the protocol logic is cheatah source; it rides on the C++-authored stdlib underneath (socket for TCP, tls for HTTPS, parsers for URL/JSON parsing).
import io
import requests
let r = requests.get("http://example.com/")
if r.ok() { io.print(r.status_code, r.text()) }
# Verbs, query params, custom headers, timeout, redirect budget.
let o = requests.Options({.timeout_ms = 5000})
o.params["q"] = "cheatah"
let s = requests.get("https://example.com/search", o)
io.print(s.ok(), s.header("content-type"))
# POST a JSON body (to_json serializes a flat dict; pass json_body for anything richer).
let body = requests.Options({.json_body = requests.to_json({"side": "buy"})})
let p = requests.post("https://api.example.com/order", body)
p.raise_for_status() # raises on 4xx/5xx (a no-op otherwise)
# Form data and Basic auth.
let f = requests.Options({.auth_user = "key", .auth_pass = "secret"})
f.data["symbol"] = "SPX"
let q = requests.post("https://api.example.com/quote", f) # application/x-www-form-urlencodedVerbs
get, post, put, patch, delete, head, options, plus the generic request(method, url, o). Each takes an optional Options; head returns headers only (empty body). delete works because purrc escapes the C++ keyword in codegen.
Request bodies (
One of, in precedence order: json_body (a pre-serialized JSON string → application/json), data (a dict<str,str> → application/x-www-form-urlencoded, percent-encoded), or body (a raw string). Content-Type and Content-Length are added automatically unless you set them yourself. GET/HEAD never carry a body. to_json(dict<str,str>) serializes the common flat-object case (build the string yourself for nested/non-string JSON).
Auth
auth_user/auth_pass add HTTP Basic (Authorization: Basic <base64>). For Bearer tokens or API-key/HMAC schemes, set the Authorization (or any) header yourself — a caller-supplied header is never overridden.
params, headers, timeout_ms (default 30000), max_redirects (default 5), no_redirect, body, data, json_body, auth_user, auth_pass, max_bytes (max response body; default 100 MiB — a hard cap so a hostile/compromised server cannot exhaust memory), insecure (https: skip TLS cert validation; default false = verify), ca_file (https: a PEM CA bundle to trust instead of the system store).
Fields status_code, reason, headers, body, url, error, cookies (parsed from Set-Cookie), history (intermediate responses when redirects were followed). Methods: ok() (2xx and no error), header(name) (case-insensitive), text()/content() (the body), json(out) (typed parse into a struct via the accelerated parsers.json.read), raise_for_status() (raises on 4xx/5xx), is_redirect(), is_permanent_redirect().
A non-2xx status is a completed exchange: error stays empty; only transport failures (DNS/connect/timeout/TLS/malformed) set error and leave status_code 0. Redirects (301/302/303/307/308) are followed by default; 303 (and 301/302 on a POST) continue as GET with the body dropped, matching Python. Header names are stored lowercased. Internally requests opens the TCP fd and (for https) a tls.Conn guard, so a request never leaks its connection even on an error path.
Security notes
https://authenticates the server by default. It rides the cheatahtlsclient, which validates the certificate chain to a trusted CA, matches the hostname against the certificate SAN, and checks expiry (see thetlsREADME) — so an active man-in-the-middle is refused, not just a passive eavesdropper. For a pinned/controlled peer, setOptions.insecure = trueto skip validation, orOptions.ca_fileto trust a specific PEM CA bundle (e.g. a private CA).Response size is capped at
Options.max_bytes(default 100 MiB): a server that streams an unbounded body — or declares an oversizedContent-Length— fails with an error instead of exhausting memory.Malformed framing never crashes. A non-numeric/overflowing/negative
Content-Lengthor a malformed status line, and an overflowing chunk size, seterror; they do not raise.Cross-host redirects drop credentials. On a redirect to a different host, Basic-auth (
auth_user/auth_pass) and anyAuthorization/Cookieheader are stripped before the next hop, so secrets scoped to the original host are never sent to another origin. (Custom auth headers likeX-Api-Keythat you set yourself are your responsibility across hosts.) The caller'sOptionsis never mutated — the request works on a private copy.
Deviations from Python
json()takes a struct (r.json(out)) and uses the accelerated schema-typed reader, rather than returning a dynamic object. A struct-free dynamicjson()for ad-hoc navigation is planned onceparsers.jsongains.purr-navigable accessors.textandcontentare identical — cheatah strings are byte-based, so there is no separate decoded-text vs bytes distinction.Redirect opt-out is
no_redirect(notallow_redirects). cheatah zero-initializes structs, so a true-by-defaultallow_redirectsbool would silently becomefalseon any hand-builtOptions. Instead redirects are followed by default (the zero value) and you set.no_redirect = trueto stop at the 3xx — the same behavior as Python'sallow_redirects=False.paramsiterate in unspecified order (dict<str,str>). If you must sign an exact query string (HMAC), put the query in the URL rather than inparams.One connection per request (
Connection: close); no keep-alive/Sessionyet.
Not yet supported
Session/keep-alive/connection reuse, retries, multipart/files=, streaming/iter_content, proxies, and explicit TLS/certificate configuration.
Functions
Does text contain a byte that would break out of the line it is written on?
The request is built by concatenating a request-target and header values into a CRLF-framed message, so a CR or LF reaching either one lets a caller inject headers or split the request entirely. That matters most when the value is not the caller's own: a URL taken from a fetched document or a Location header is attacker-controlled data, and nothing else on the path re-checks it. Refusing here means no consumer of this module can be made to emit a forged request, whatever it was handed.
text | a request-target or header value about to be written to the wire. |
true when text carries CR or LF and must not be sent.
O(n) over the input length.
none.
CheatahRequests.CrlfInjectionRefusedSerialize a flat string->string dict as a JSON object — the common json= case.
For nested or non-string JSON, build the string yourself and pass it as json_body.
fields | the name -> value pairs. |
a JSON object string ({"k":"v",…}).
O(total characters).
allocates the result.
RequestsSys.PostJsonrequest · 2 overloads
Perform an HTTP request, following up to max_redirects 3xx hops unless no_redirect.
On a redirect to a DIFFERENT host, Basic-auth credentials and any Authorization/Cookie header are stripped before the next hop, so secrets are never leaked to another origin. Never raises for network conditions: every failure comes back as a Response with error set (and status_code 0). Redirects (301/302/303/307/308) follow absolute and host-relative Location targets, recording each hop in the returned Response's history; 303 (and 301/302 on a POST) switch the method to GET and drop the body, matching Python. Set o.no_redirect = true to return the 3xx response directly.
method | the HTTP method ("GET", "POST", …). |
url | the absolute |
o | per-request options; defaults to a 30 s timeout and 5 redirect hops (redirects followed unless no_redirect). |
the final Response — check ok(), then status_code/headers/body.
one full exchange (request_once) per hop, at most 1 + max_redirects hops.
allocates each hop's request/response buffers, the recorded history, and a private copy of o (so redirect-time credential stripping never mutates the caller's).
blocking, with every hop's socket I/O bounded by timeout_ms; no shared state — concurrent requests from separate threads are independent.
get · 2 overloads
HTTP GET.
url | the URL. |
o | per-request options. |
the final Response.
one request plus any redirects.
request/response buffers.
RequestsSys.BasicGetpost · 2 overloads
HTTP POST.
url | the URL. |
o | per-request options (body via json_body/data/body). |
the final Response.
one request plus any redirects.
request/response buffers.
RequestsSys.PostJsonput · 2 overloads
HTTP PUT.
url | the URL. |
o | per-request options (body via json_body/data/body). |
the final Response.
one request plus any redirects.
request/response buffers.
RequestsSys.PostJsonpatch · 2 overloads
HTTP PATCH.
url | the URL. |
o | per-request options (body via json_body/data/body). |
the final Response.
one request plus any redirects.
request/response buffers.
RequestsSys.PostJsondelete_ · 2 overloads
HTTP DELETE.
url | the URL. |
o | per-request options. |
the final Response.
one request plus any redirects.
request/response buffers.
RequestsSys.Deletehead · 2 overloads
HTTP HEAD (headers only, no body).
url | the URL. |
o | per-request options. |
the final Response (empty body).
one request plus any redirects.
request/response buffers.
RequestsSys.Headoptions · 2 overloads
HTTP OPTIONS.
url | the URL. |
o | per-request options. |
the final Response.
one request plus any redirects.
request/response buffers.
CheatahRequests.VerbMethods