{"id":1085,"date":"2026-10-02T11:16:02","date_gmt":"2026-10-02T09:16:02","guid":{"rendered":"https:\/\/www.mariusb.net\/blog\/?p=1085"},"modified":"2026-10-02T11:17:24","modified_gmt":"2026-10-02T09:17:24","slug":"curl-works-reqwest-fails-cloudflare","status":"publish","type":"post","link":"https:\/\/www.mariusb.net\/blog\/2026\/10\/curl-works-reqwest-fails-cloudflare\/","title":{"rendered":"Why curl Works but Rust&#8217;s reqwest Gets Blocked by Cloudflare"},"content":{"rendered":"<div class=\"wp-block-image\">\n<figure class=\"alignleft size-large is-resized\"><a href=\"https:\/\/i0.wp.com\/www.mariusb.net\/blog\/wp-content\/uploads\/2026\/10\/CurlReqwest.jpg?ssl=1\" rel=\"lightbox[1085]\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"559\" src=\"https:\/\/i0.wp.com\/www.mariusb.net\/blog\/wp-content\/uploads\/2026\/10\/CurlReqwest.jpg?resize=1024%2C559&#038;ssl=1\" alt=\"\" class=\"wp-image-1084\" style=\"aspect-ratio:1.8318737860769414;width:313px;height:auto\" srcset=\"https:\/\/i0.wp.com\/www.mariusb.net\/blog\/wp-content\/uploads\/2026\/10\/CurlReqwest.jpg?resize=1024%2C559&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.mariusb.net\/blog\/wp-content\/uploads\/2026\/10\/CurlReqwest.jpg?resize=300%2C164&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.mariusb.net\/blog\/wp-content\/uploads\/2026\/10\/CurlReqwest.jpg?resize=768%2C419&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.mariusb.net\/blog\/wp-content\/uploads\/2026\/10\/CurlReqwest.jpg?w=1408&amp;ssl=1 1408w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/figure>\n<\/div>\n\n\n<div class=\"wp-block-jetpack-markdown\"><p>I hit a puzzling problem while building a small price tracker in Rust. The same CoinGecko API call, with the same URL and the same API key, behaved completely differently depending on the client:<\/p>\n<ol>\n<li><strong>curl<\/strong> returned the correct JSON with the correct values.<\/li>\n<li><strong>A Rust program using <code>tokio<\/code> and <code>reqwest<\/code><\/strong> failed with a Cloudflare error.<\/li>\n<\/ol>\n<p>If the request is logically identical, why does one succeed and the other fail? Because Cloudflare doesn\u2019t decide based only on the URL and the key. It looks at <em>how<\/em> the request is made.<\/p>\n<h2>Why curl passes and reqwest doesn\u2019t<\/h2>\n<h3>1. Missing or different User-Agent (the most common cause)<\/h3>\n<p>curl automatically sends <code>User-Agent: curl\/8.x<\/code> and <code>Accept: *\/*<\/code>. <code>reqwest<\/code> sends <strong>no User-Agent at all<\/strong> unless you set one.<\/p>\n<p>Cloudflare\u2019s WAF and Bot Fight Mode frequently block empty or suspicious User-Agents, producing errors such as:<\/p>\n<ul>\n<li><strong>Error 1010<\/strong>: browser signature banned<\/li>\n<li><strong>Error 1020<\/strong>: access denied<\/li>\n<\/ul>\n<h3>2. TLS fingerprint (JA3\/JA4)<\/h3>\n<p>Cloudflare fingerprints the TLS handshake: cipher suite order, extensions, curve preferences, and more. curl typically uses OpenSSL or similar, while <code>reqwest<\/code> uses <code>rustls<\/code> or <code>native-tls<\/code> depending on the enabled features. The fingerprints differ, and some Cloudflare configurations score unfamiliar ones as bot-like.<\/p>\n<h3>3. HTTP\/2 fingerprint and header ordering<\/h3>\n<p>Beyond TLS, Cloudflare can inspect HTTP\/2 SETTINGS frames, header order, and pseudo-header order. These differ between curl (which uses nghttp2) and Rust\u2019s <code>hyper<\/code>\/<code>h2<\/code> stack.<\/p>\n<h3>4. Header differences<\/h3>\n<p>curl sends a specific set of headers in a specific order. Your Rust client may omit <code>Accept<\/code>, send a different <code>Accept-Encoding<\/code>, or set <code>Content-Type<\/code> differently, any of which can trip a rule.<\/p>\n<h3>5. Environment differences<\/h3>\n<p>If the Rust program runs somewhere other than where you ran curl (a VPS, a container, CI, or a different network), IP reputation and rate limits may differ. That can surface as <strong>Error 1015<\/strong> (rate limited) or a challenge page.<\/p>\n<h3>6. Redirect and auth handling<\/h3>\n<p><code>reqwest<\/code> strips the <code>Authorization<\/code> header when following a redirect to a different host. If the API redirects, you may silently lose authentication.<\/p>\n<h2>Diagnosing the problem<\/h2>\n<p>Before changing anything, read the actual response rather than just the error:<\/p>\n<pre><code class=\"language-rust\">let resp = client.get(url).send().await?;\nprintln!(&quot;status: {}&quot;, resp.status());\nprintln!(&quot;headers: {:#?}&quot;, resp.headers());\nprintln!(&quot;body: {}&quot;, resp.text().await?);\n<\/code><\/pre>\n<p>The HTML body of a Cloudflare error page usually names the error code (1010, 1020, 1015, and so on), which tells you which of the causes above applies. It also helps to compare <code>curl -v<\/code> output against your Rust request to see which headers curl sends that you don\u2019t.<\/p>\n<h2>The fix<\/h2>\n<h3>The original code<\/h3>\n<p>My first version used <code>reqwest::get<\/code>, which creates a throwaway default client with no User-Agent:<\/p>\n<pre><code class=\"language-rust\">let url = format!(\n    &quot;https:\/\/api.coingecko.com\/api\/v3\/simple\/price?ids=cardano,midnight-3,blockdag,tron,dogecoin,binancecoin,ethereum,tether,usd-coin&amp;vs_currencies=btc&amp;x_cg_demo_api_key={api_key}&quot;\n);\nlet resp = reqwest::get(&amp;url).await?;\nlet data = resp.json::&lt;PriceMultiResponse&gt;().await?;\nOk(data)\n<\/code><\/pre>\n<h3>Building a proper client<\/h3>\n<p>Build a <code>Client<\/code> with a User-Agent and an <code>Accept<\/code> header, then send the request through it:<\/p>\n<pre><code class=\"language-rust\">use reqwest::header::{HeaderMap, HeaderValue, ACCEPT};\n\nlet url = format!(\n    &quot;https:\/\/api.coingecko.com\/api\/v3\/simple\/price?ids=cardano,midnight-3,blockdag,tron,dogecoin,binancecoin,ethereum,tether,usd-coin&amp;vs_currencies=btc&amp;x_cg_demo_api_key={api_key}&quot;\n);\n\nlet mut headers = HeaderMap::new();\nheaders.insert(ACCEPT, HeaderValue::from_static(&quot;application\/json&quot;));\n\nlet client = reqwest::Client::builder()\n    .user_agent(concat!(env!(&quot;CARGO_PKG_NAME&quot;), &quot;\/&quot;, env!(&quot;CARGO_PKG_VERSION&quot;)))\n    .default_headers(headers)\n    .build()?;\n\nlet resp = client.get(&amp;url).send().await?;\nlet data = resp.json::&lt;PriceMultiResponse&gt;().await?;\nOk(data)\n<\/code><\/pre>\n<p>A few notes on this version:<\/p>\n<ul>\n<li><code>concat!(env!(&quot;CARGO_PKG_NAME&quot;), ...)<\/code> builds a User-Agent like <code>my-app\/0.1.0<\/code> from your <code>Cargo.toml<\/code>. Any fixed string such as <code>&quot;my-app\/1.0&quot;<\/code> works too.<\/li>\n<li><strong>Build the <code>Client<\/code> once and reuse it.<\/strong> Store it in a struct or pass it in rather than rebuilding it on every call. It pools connections internally.<\/li>\n<li>If it still fails, add <code>.error_for_status()?<\/code> after <code>.send().await?<\/code>, or print <code>resp.status()<\/code> and the body before calling <code>.json()<\/code>.<\/li>\n<\/ul>\n<h3>Bonus: keep the API key out of the URL<\/h3>\n<p>CoinGecko also accepts the demo key as a header (<code>x-cg-demo-api-key<\/code>) instead of a query parameter. That keeps the key out of URLs and logs, and <code>.query()<\/code> handles encoding for you:<\/p>\n<pre><code class=\"language-rust\">let resp = client\n    .get(&quot;https:\/\/api.coingecko.com\/api\/v3\/simple\/price&quot;)\n    .query(&amp;[\n        (&quot;ids&quot;, &quot;cardano,midnight-3,blockdag,tron,dogecoin,binancecoin,ethereum,tether,usd-coin&quot;),\n        (&quot;vs_currencies&quot;, &quot;btc&quot;),\n    ])\n    .header(&quot;x-cg-demo-api-key&quot;, &amp;api_key)\n    .send()\n    .await?;\n<\/code><\/pre>\n<h2>If it\u2019s still blocked<\/h2>\n<p>Work through these in order:<\/p>\n<ol>\n<li><strong>Set a User-Agent and <code>Accept<\/code> header.<\/strong> This fixes the problem most of the time.<\/li>\n<li><strong>Switch TLS backends<\/strong> by toggling between <code>rustls-tls<\/code> and <code>native-tls<\/code> in your <code>Cargo.toml<\/code> features.<\/li>\n<li><strong>Force HTTP\/1.1<\/strong> with <code>.http1_only()<\/code> on the builder, to rule out HTTP\/2 fingerprinting.<\/li>\n<li><strong>Run from the same machine and network<\/strong> as your successful curl test.<\/li>\n<li><strong>Contact the API provider.<\/strong> Ask them to allowlist your client or tell you which Cloudflare rule is blocking it.<\/li>\n<\/ol>\n<p>A word of caution on that last point: spoofing a browser\u2019s fingerprint to evade bot protection is fragile and may violate the provider\u2019s terms. An honest, identifiable User-Agent is the better long-term approach.<\/p>\n<h2>Takeaway<\/h2>\n<p>When \u201ccurl works but my code doesn\u2019t,\u201d the difference is rarely the URL or the key. It\u2019s usually the invisible parts of the request: headers, TLS, and HTTP\/2 behavior. Start by setting a proper User-Agent, read the real error body, and only go deeper if you need to.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Same URL, same API key, different result. A walkthrough of why Cloudflare treats curl and reqwest differently, and how to fix it.<\/p>\n","protected":false},"author":1,"featured_media":1084,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"advanced_seo_description":"Debug why Cloudflare blocks Rust's reqwest while curl succeeds. Learn how User-Agent, TLS fingerprints, and HTTP\/2 settings cause Error 1020\/1010\u2014and how to fix it.","jetpack_seo_html_title":"Why curl Works but Rust's reqwest Fails with Cloudflare","jetpack_seo_noindex":false,"jetpack_seo_schema_type":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":true,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[193,171,188],"tags":[204,205,202,206,189,203],"class_list":["post-1085","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","category-cryptocurrencies","category-rust","tag-api","tag-cloudflare","tag-curl","tag-reqwest","tag-rust","tag-tokio"],"jetpack_publicize_connections":[],"jetpack_shortlink":"https:\/\/wp.me\/p1SHVw-hv","jetpack_sharing_enabled":true,"jetpack_featured_media_url":"https:\/\/i0.wp.com\/www.mariusb.net\/blog\/wp-content\/uploads\/2026\/10\/CurlReqwest.jpg?fit=1408%2C768&ssl=1","_links":{"self":[{"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/posts\/1085","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/comments?post=1085"}],"version-history":[{"count":3,"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/posts\/1085\/revisions"}],"predecessor-version":[{"id":1088,"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/posts\/1085\/revisions\/1088"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/media\/1084"}],"wp:attachment":[{"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/media?parent=1085"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/categories?post=1085"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.mariusb.net\/blog\/wp-json\/wp\/v2\/tags?post=1085"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}