curl_reap / Guides
Troubleshooting and limits
Fix common curl_reap problems: wrong version, decoding and certificate errors, blocks, doctor mismatches, known issues on main and limits by design.
On this page
This page collects the errors and surprises users run into, with what causes each one and what to do. Every message and limit here comes from the source on main.
First principles
Most problems fall into one of three groups, and each group has a quick check:
- The request took a different path than you expected. Another TLS backend, another protocol, another version of the library. Check
r.infos["tls_backend"],r.http_versionandreap.__version__. - The server answered, but not with the page. A block, a challenge or a rate limit is a normal response with a status and a body. Check
r.statusandr.block_reason. - You reached a deliberate limit. Body caps, timeouts, the crawl budget and the queue bound are there to keep a long job from running away. The error message names the limit.
Quick checks
import logging
import curl_reap as reap
logging.basicConfig(level=logging.INFO) # debug=True logs to "curl_reap.transport"
r = reap.get("https://example.com", debug=True)
print(reap.__version__) # "2.0.0" for these docs
print(r.status, r.http_version, r.infos["tls_backend"], r.block_reason)
reap doctor chrome # compare your fingerprint with the capture
debug=True logs every request and response header line at INFO level. Transport Layer Security (TLS) details are in r.infos; see What Response.infos holds.
Installation
reap.__version__is not2.0.0. A plainpip install curl_reapreads the Python Package Index (PyPI), which serves 0.2.2, an older release built on curl_cffi. Install from themainbranch as shown in Installation.- The git install fails. pip needs
giton yourPATHto install from a git URL. ImportError: JS rendering needs Playwright. Install thejsextra, then runplaywright install chromium.reap serveorreap workerexits with status 2. A required variable (REAP_API_KEY,REAP_ALLOWED_ORIGINS) is missing, theserverextra is not installed, an origin is invalid, or Redis could not be set up. The message on standard error says which. See Private job service.
Request errors
ContentDecodingErroron azstdbody. 41 of the 79 targets, includingchrome(Chrome 123 and newer) and every Firefox target, advertisezstdinAccept-Encoding. Decoding a bufferedzstdbody needs thezstdextra on Python 3.13 and older. Streamed bodies (stream=True) and downloads cannot decodezstdat all. For those, sendheaders={"Accept-Encoding": "gzip, deflate, br"}.ValueError: tls='native' cannot send a client certificate. The native engine cannot present a client certificate. Usetls="auto"(the default), which sendscert=requests through OpenSSL. See Client certificates.NotImplementedError: HTTP/3 (QUIC) is not implemented.http_version="v3"and"v3only"are accepted names, but HTTP/3 does not exist in curl_reap.TypeError: unexpected request keyword argument. The keyword is not a request option. The accepted keywords are listed in Per-request options.ValueError: profile and impersonate disagree; pass one. A request passed both with names that resolve to different targets. Pass one of them.UnknownProfileError. The target name does not exist.reap doctor --listprints all 79 names.TransportError: ... exceeds max_body_bytes=67108864. A buffered body is over the 64 mebibyte (MiB) cap. Raisemax_body_bytesfor HTTP/1.1, or usedownload()orstream=True. Over HTTP/2 the buffered cap is fixed. This error is aTransportError, so the session retries it like a network error; passretries=0when a large body is expected.ConnectTimeoutorReadTimeout. A numbertimeoutbounds connect, send and receive together; a(connect, read)tuple bounds the connect and each read. The clock restarts on every redirect hop and retry. See Timeouts.TooManyRedirectson a short chain. Withallow_redirects="safe", a redirect to a private, loopback or link-local address is refused with this error. The redirect response is onexc.response.URLPolicyError. Yoururl_policycallable returned a falsy value for the request URL or a redirect hop.CookieConflictfroms.cookies.get(name). Several cookies share the name on different domains or paths. Passdomain=orpath=.
Blocks and challenges
r.block_reasonis"forbidden","proxy_auth_required"or"rate_limited". The server answered 403, 407 or 429. With a proxy pool, the session already switched proxies up toblock_rotationstimes before returning this. A 429 is also retried with itsRetry-Aftervalue.r.block_reasonis"challenge". The body looks like a bot-wall interstitial.render_if_emptydoes not render these, and curl_reap does not solve them. Read Responsible use.- A crawl callback never sees some pages. Responses with status 400 or higher and challenge-looking responses go to the request’s errback, not to
parse. See Crawling. r.okisTruefor a redirect. In 2.0,okcovers 200 to 399. User.is_successfor 2xx only.
Fingerprint checks
reap doctorshowsn/a.ja3is not comparable for browsers that permute their extensions on each connection, andheadersis not comparable when the service did not echo them. Ann/arow does not fail the check.reap doctorshowsDIFFERS. Something changed the bytes between curl_reap and the service. Common causes: a proxy or security product that terminates TLS and sends its own handshake, headers you set yourself, or aja3=orakamai=override. The expected values are printed to standard error.r.infos["tls_backend"]is"openssl". The connection did not use the captured handshake. That happens when the server only speaks TLS 1.2, when the request sendscert=, with a customProfilethat has no template, or withtls="openssl". The HTTP layer still follows the profile.
Known issues on main
These are confirmed from the source on main as of 2026-10-25.
TLS 1.2 servers and proxies
When the native handshake finds that the server only speaks TLS 1.2, the transport reconnects for the OpenSSL fallback. That reconnect does not go back through every kind of proxy:
- SOCKS proxies: the fallback connection goes directly to the server, not through the Socket Secure (SOCKS) proxy, so the request leaves from your own address. Only the first, native attempt goes through the proxy.
- HTTP proxies: a direct connection to the server is opened and closed before the request is tunnelled through the proxy.
- HTTPS proxies: the request fails with
TransportError: TLS-1.2-only servers cannot be combined with an https proxy.
Until this is fixed, use tls="openssl" on sessions that must never leave the proxy and may reach TLS 1.2 servers. Those connections use OpenSSL’s handshake from the start, through the proxy, at the cost of the captured TLS fingerprint.
Other known issues
- A custom
Profileand HTTP/2. A hand-built profile has no HTTP/2 spec, but the transport still offersh2. If the server picks it, the request is written as HTTP/1.1 on that connection. Passhttp2=Falsewith custom profiles. - Removing a profile header. A per-request header set to
Noneremoves a session header but not one that comes from the profile. Use a custom profile to send fewer headers. stream=Trueover HTTP/2 buffers the body and replays it. Passhttp2=Falsefor a real stream.AsyncWebSocketdoes not accept keyword arguments onsend,send_jsonandclose. See WebSocket.- Cancelling an async request stops the wait but not a socket call already in progress; the request timeout still bounds it.
Limits by design
- No HTTP/3, no native TLS 1.2, no cleartext HTTP/2, and no HTTP/2 multiplexing: one request per connection at a time.
- Buffered bodies: 64 MiB by default over HTTP/1.1 (
max_body_bytes), fixed at 64 MiB over HTTP/2. Downloads have no cap. - The crawl queue lives in memory. A crash loses pending requests; there is no resume.
- Cookies are scoped by domain, path and name, but public-suffix lists are not checked.
- WebSocket: no
permessage-deflatecompression, and a single incoming frame is capped at 64 MiB. - Job service: one page per job, results up to 1 MiB, at most 1000 jobs queued and running.