# curl_cffi proxies: auth, the 407 that ProxyError misses, and SOCKS

Source: https://ipvolt.com/guides/curl-cffi-proxy-setup
Markdown: https://ipvolt.com/guides/curl-cffi-proxy-setup.md

[Home](https://ipvolt.com/index.md) / [Guides](https://ipvolt.com/guides.md) / [curl_cffi proxies: auth, the 407 that ProxyError misses, and SOCKS](https://ipvolt.com/guides/curl-cffi-proxy-setup.md)

Category: Integration
Published: 2026-10-10
Reading time: 8 minutes
Author: ipvolt

curl_cffi proxy setup on 0.16.3: credentials with impersonate, why a CONNECT 407 raises ConnectionError and not ProxyError, HTTP_PROXY rules, socks5h.

Put the credentials in the proxy URL and pass it as `proxy`:

```python
from curl_cffi import requests

PROXY = "http://user:pass@proxy.example.com:8080"

with requests.Session(impersonate="chrome", proxy=PROXY) as session:
    response = session.get("https://example.com/")
    print(response.status_code)
```

To catch proxy failures, catch `ProxyError` and `ConnectionError`, and check `status_code` for 407. In curl_cffi 0.16 a proxy that rejects `CONNECT` raises `ConnectionError`, not `ProxyError`.

In our lab this authenticated `https://` and `http://` requests against a local proxy that requires Basic authentication. Everything below was recorded on 10 October 2026 with curl_cffi 0.16.3 (libcurl 8.21.0) on Python 3.14.4. `impersonate` selects the browser profile curl_cffi copies for its TLS and HTTP/2 settings. Every result below was the same with and without it.

Three details before the errors:

- **Use `proxy` or `proxies`, not both.** `proxies={"http": PROXY, "https": PROXY}` worked the same on a request and on a `Session`. The key is the scheme of the URL you request: with only an `"https"` key, an `http://` request went direct. Passing both arguments raised `TypeError: Cannot specify both 'proxy' and 'proxies'`.
- **Percent-encode the password in the URL.** `p@ss:w/rd` worked as `p%40ss%3Aw%2Frd`. Unencoded, the request raised `ProxyError` with `curl: (5) Unsupported proxy syntax`. `proxy_auth=("user", "p@ss:w/rd")` next to a proxy URL without credentials also worked and needs no encoding.
- **Write `http://` for an HTTP proxy, for `https://` URLs too.** The scheme describes the connection to the proxy. An `https://` proxy URL pointed at the lab's plain HTTP proxy raised `SSLError` with `curl: (35) TLS connect error` and `WRONG_VERSION_NUMBER`. [SSL WRONG_VERSION_NUMBER behind a proxy](/guides/fix-ssl-wrong-version-number-proxy) covers that message.

## CONNECT tunnel failed, response 407: ConnectionError, not ProxyError

```text
curl_cffi.requests.exceptions.ConnectionError: Failed to perform, curl: (7) CONNECT tunnel failed, response 407. See https://curl.se/libcurl/c/libcurl-errors.html first for more details.
```

For an `https://` URL curl_cffi first sends `CONNECT` to the proxy. The lab proxy answered 407 and the request raised this error. The message was identical with no credentials and with a wrong password.

- `isinstance(error, ProxyError)` is `False`. `ProxyError` and `ConnectionError` are siblings under `RequestException`, so `except ProxyError` does not catch it.
- `error.code` is 7.
- `error.response` is a `Response` whose `status_code` is 0, not 407. The status is only in the message.
- Code 7 is not specific to a 407. When the lab proxy could not reach the target it answered 502, and the message was `curl: (7) CONNECT tunnel failed, response 502`. With nothing listening on the proxy port it was `curl: (7) Failed to connect to 127.0.0.1:8443 over proxy 127.0.0.1 after 0 ms: Could not connect to server`.

The class follows the curl error code, and that code changed between releases. We ran the same request on twelve of them:

| curl_cffi | libcurl | Rejected `CONNECT` | SOCKS failure |
| --- | --- | --- | --- |
| 0.16.0 to 0.16.3 | 8.21.0 | `ConnectionError`, curl code 7 | `ProxyError`, curl code 97 |
| 0.7.4 to 0.15.0 | 8.7.0 to 8.15.0 | `ProxyError`, curl code 56 | `ProxyError`, curl code 97 |

Up to 0.15.0 the message was `curl: (56) CONNECT tunnel failed, response 407`. The curl command line moved from exit code 56 to 7 in 8.18.0. [Fix proxy error 407](/guides/fix-proxy-error-407) has that table and the order of checks for the credentials.

This block handles a rejected `CONNECT` on both sides of the change, a SOCKS failure and a 407 response:

```python
import re

from curl_cffi.requests.exceptions import ConnectionError, ProxyError

CONNECT_STATUS = re.compile(r"CONNECT tunnel failed, response (\d{3})")


class ProxyFailure(Exception):
    pass


def fetch(session, url):
    try:
        response = session.get(url)
    except ProxyError as error:
        raise ProxyFailure(f"proxy error, curl {error.code}") from error
    except ConnectionError as error:
        rejected = CONNECT_STATUS.search(str(error))
        if error.code == 7 and rejected:
            raise ProxyFailure(f"proxy answered CONNECT with {rejected[1]}") from error
        raise
    if response.status_code == 407:
        raise ProxyFailure("proxy answered 407")
    return response
```

In the lab it reported `proxy answered CONNECT with 407`, `proxy answered CONNECT with 502`, `proxy answered 407` for an `http://` URL and `proxy error, curl 97` for a SOCKS failure. It re-raised the `ConnectionError` for a closed proxy port.

## 407 on http:// URLs: a response, not an exception

For an `http://` URL there is no tunnel. curl_cffi sends the full URL to the proxy in a plain `GET`, as it has since 0.7.0b7 ([issue #326](https://github.com/lexiforest/curl_cffi/issues/326)), and the proxy's 407 comes back as the response.

No exception was raised. `response.status_code` was 407, `response.ok` was `False` and the headers carried `Proxy-Authenticate: Basic realm="lab"`. Code that only catches exceptions treats this as a successful request with an empty body.

`response.raise_for_status()` raised `curl_cffi.requests.exceptions.HTTPError: HTTP Error 407: Proxy Authentication Required`. On that exception `error.response.status_code` is 407.

## Environment variables: http_proxy is read, HTTP_PROXY is not

With no proxy in the code, libcurl picks one from the environment:

- For an `http://` URL, uppercase `HTTP_PROXY` was ignored and the request went direct. Lowercase `http_proxy` sent it through the proxy.
- For an `https://` URL, `HTTPS_PROXY` and `https_proxy` were both used. `http_proxy` alone left an `https://` request direct.
- `ALL_PROXY` and `all_proxy` applied to both schemes.
- `NO_PROXY=127.0.0.1` and lowercase `no_proxy` sent the request direct.

libcurl documents the exception: the variables are also checked in uppercase, "except the http_proxy one which is only used lowercase" ([libcurl environment variables](https://curl.se/libcurl/c/libcurl-env.html)). The reason is CGI: a server turns a `Proxy:` request header into an `HTTP_PROXY` variable for the script ([Everything curl](https://everything.curl.dev/usingcurl/proxies/env.html)).

Scrapy reads uppercase `HTTP_PROXY` ([Scrapy proxies in 2.19](/guides/scrapy-proxy-setup)), and aiohttp reads no variable unless you pass `trust_env=True` ([aiohttp proxies in 3.14](/guides/aiohttp-proxy-setup)). [Proxy environment variables](/guides/proxy-environment-variables) compares more clients.

Two results are easy to miss:

- **`NO_PROXY` overrides the `proxy` argument.** With `NO_PROXY=127.0.0.1` set, `requests.get(url, proxy=PROXY)` went direct for both schemes. Adding `curl_options={CurlOpt.NOPROXY: ""}` sent it through the proxy again. Against `http_proxy` the argument won.
- **`trust_env=False` did not turn the variables off.** A `Session(trust_env=False)` still used `http_proxy` and `HTTPS_PROXY`. So did `proxy=""`.

Two settings did send the request direct with the variables set:

```python
from curl_cffi import CurlOpt, requests

requests.get("https://example.com/", proxies={"all": ""})
requests.get("https://example.com/", curl_options={CurlOpt.NOPROXY: "*"})
```

Both also work as `Session` arguments. An empty proxy string "explicitly disables the use of a proxy, even if there is an environment variable set for it" ([CURLOPT_PROXY](https://curl.se/libcurl/c/CURLOPT_PROXY.html)).

## SOCKS: socks5:// resolves locally, socks5h:// sends the hostname

The lab requested `http://dnsprobe.example:8080/`, a name that exists only in the hosts file as `127.0.0.2`, through a SOCKS5 server that logs each destination:

- With `socks5://127.0.0.1:1080` the server logged `CONNECT atyp=IPv4 dest=127.0.0.2:8080`. curl_cffi resolved the name itself.
- With `socks5h://127.0.0.1:1080` it logged `CONNECT atyp=DOMAIN dest=dnsprobe.example:8080`. The proxy received the hostname.

An `https://` URL gave the same two results. This is curl's own convention ([Use a proxy with curl](/guides/curl-proxy-setup)). [socks5 vs socks5h](/blog/socks5-vs-socks5h) lists clients that do not follow it.

A failed SOCKS handshake is the case that does raise `ProxyError`:

```text
curl_cffi.requests.exceptions.ProxyError: Failed to perform, curl: (97) cannot complete SOCKS5 connection to 127.0.0.1. (1). See https://curl.se/libcurl/c/libcurl-errors.html first for more details.
```

`error.code` is 97. The last number in parentheses is the reply code the SOCKS server sent: the lab server answered 1, general failure. When the target port was closed and the server answered 5, the message ended in `(5)`.

A scheme that does not match the port fails in other ways:

- `socks5://` pointed at the HTTP proxy port raised `ProxyError` with `curl: (97) Received invalid version in initial SOCKS5 response.`
- `http://` pointed at the SOCKS port raised `ProxyError` with `curl: (56) Proxy CONNECT aborted` for an `https://` URL.
- With nothing listening on the SOCKS port, the error was the `ConnectionError` with code 7 again.

## AsyncSession: the same setup, the same errors

```python
import asyncio

from curl_cffi import AsyncSession

PROXY = "http://user:pass@proxy.example.com:8080"


async def main():
    async with AsyncSession(impersonate="chrome", proxy=PROXY) as session:
        response = await session.get("https://example.com/")
        print(response.status_code)


asyncio.run(main())
```

Every async case matched its sync twin. A rejected `CONNECT` raised the same `ConnectionError` with code 7. An `http://` URL returned the 407 as a response. A SOCKS failure raised `ProxyError` with code 97. `socks5://` and `socks5h://` sent the same address types, and `HTTP_PROXY` was ignored while `http_proxy` was used.

## Retries: retry repeats POST requests

curl_cffi does not retry by default. The lab sent a `POST` through the proxy to a target that reads the request and closes the connection without answering. The call raised `ConnectionError` with `curl: (52) Empty reply from server`, and the target had received one `POST`.

With `requests.Session(proxy=PROXY, retry=1)` the same call returned 201, and the target had received the body twice. `retry` repeated the `POST` the way it repeats a `GET`.

`retry=2` on a rejected `CONNECT` sent three `CONNECT` requests and raised the same error, with `Session` and with `AsyncSession`. A retry does not fix wrong credentials.

Keep `retry` at 0 for requests that are not safe to repeat, or send an idempotency key. [Proxy retries: one lost response, two created jobs](/blog/proxy-retries-duplicate-jobs) shows what a key changes. For diagnostic help inside a coding agent, [Proxy Toolkit MCP](/mcp) accepts sanitized error descriptions. Keep credentials out of its arguments.

## How this was tested

On Ubuntu 26.04.1 with Python 3.14.4 and curl_cffi 0.16.3 (libcurl 8.21.0-IMPERSONATE with BoringSSL), everything on loopback: `python -m http.server` as the HTTP and HTTPS targets, three proxies that require Basic authentication and log every request, a SOCKS5 server that logs each destination and one that answers every request with reply code 1, and a target that loses the response to the first `POST`. `dnsprobe.example` existed only in a private copy of the hosts file. Each of the 86 cases ran in a new interpreter with a clean environment. The version comparison installed twelve releases from 0.7.4 to 0.16.3. No proxy account was used.

The [lab archive](https://ipvolt.com/downloads/curl-cffi-proxy-setup/curl-cffi-proxy-lab.zip) contains the [runner](https://ipvolt.com/downloads/curl-cffi-proxy-setup/run_lab.py), the [cases](https://ipvolt.com/downloads/curl-cffi-proxy-setup/cases.py), the [proxy](https://ipvolt.com/downloads/curl-cffi-proxy-setup/auth_proxy.py), the [SOCKS5 server](https://ipvolt.com/downloads/curl-cffi-proxy-setup/socks5_server.py), the [README](https://ipvolt.com/downloads/curl-cffi-proxy-setup/README.md), the [recorded results](https://ipvolt.com/downloads/curl-cffi-proxy-setup/results.json) and the [version comparison](https://ipvolt.com/downloads/curl-cffi-proxy-setup/versions.json).

Not tested: macOS and Windows, Digest and NTLM proxy authentication, authenticated SOCKS proxies, proxies reached over TLS, HTTP/3 and WebSockets.

ipvolt is a proxy service for developers that isn't open yet; [join the early-access list](https://ipvolt.com/#waitlist-closing) for one email when it opens.

## Sources & further reading

- [curl_cffi documentation: Proxies](https://curl-cffi.readthedocs.io/en/latest/advanced.html#proxies)
- [curl_cffi API reference: Session (proxy, proxies, proxy_auth, trust_env, retry)](https://curl-cffi.readthedocs.io/en/latest/api.html#curl_cffi.requests.Session)
- [curl_cffi API reference: AsyncSession](https://curl-cffi.readthedocs.io/en/latest/api.html#curl_cffi.requests.AsyncSession)
- [curl_cffi API reference: Exceptions and warnings (ProxyError, ConnectionError)](https://curl-cffi.readthedocs.io/en/latest/api.html#exceptions-and-warnings)
- [curl_cffi documentation: Using CURLOPT_* in requests API (curl_options)](https://curl-cffi.readthedocs.io/en/latest/advanced.html#using-curlopt-in-requests-api)
- [curl_cffi issue #326: CONNECT was sent for http:// URLs, changed in 0.7.0b7](https://github.com/lexiforest/curl_cffi/issues/326)
- [libcurl: CURLOPT_PROXY (schemes, the empty string, socks5h)](https://curl.se/libcurl/c/CURLOPT_PROXY.html)
- [libcurl: CURLOPT_NOPROXY and the no_proxy variable](https://curl.se/libcurl/c/CURLOPT_NOPROXY.html)
- [libcurl environment variables: http_proxy is only used lowercase](https://curl.se/libcurl/c/libcurl-env.html)
- [Everything curl: proxy environment variables and the CGI reason](https://everything.curl.dev/usingcurl/proxies/env.html)
- [libcurl error codes: CURLE_COULDNT_CONNECT (7), CURLE_RECV_ERROR (56), CURLE_PROXY (97)](https://curl.se/libcurl/c/libcurl-errors.html)

## Related guides

- [Fix proxy error 407 without guessing](https://ipvolt.com/guides/fix-proxy-error-407.md)
- [Use a proxy with curl: -x, env vars, SOCKS5, auth](https://ipvolt.com/guides/curl-proxy-setup.md)
- [Proxy environment variables: HTTP_PROXY and NO_PROXY](https://ipvolt.com/guides/proxy-environment-variables.md)
- [aiohttp proxies in 3.14: auth without BasicAuth, env vars and the errors you'll hit](https://ipvolt.com/guides/aiohttp-proxy-setup.md)
- [Scrapy proxies in 2.19: auth, rotation and the errors you'll hit](https://ipvolt.com/guides/scrapy-proxy-setup.md)
- [Python Requests proxy: proxies dict, auth, SOCKS5](https://ipvolt.com/guides/python-requests-proxy.md)

## About ipvolt

Examples use generic proxy settings, with links to the original technical documentation. Product-specific behavior must be checked with your provider. ipvolt is still in development.

## Know when access opens.

ipvolt · In development

We’re building proxy infrastructure for developers and data teams. Join the interest list for a heads-up when ipvolt is ready.

Consent: One email when access opens. Nothing else.

[Get early access](https://ipvolt.com/guides/curl-cffi-proxy-setup#waitlist-closing). Use the email form on this page to join the interest list.

[Privacy](https://ipvolt.com/privacy)

