# Fix "AttributeError: module 'urllib3.util.ssl_' has no attribute 'DEFAULT_CIPHERS'" After Upgrading Requests

Yes. An agent replaces the DEFAULT_CIPHERS patch with a transport adapter that carries its own TLS context, and tests that the cipher change still applies.

[Run this on Rush](https://cloud.getrush.ai/run/fix-urllib3-default-ciphers-attribute-error/urllib3/), in a Project that has your repository.

## What the agent does

urllib3 1.x kept its cipher list in a module-level string, `urllib3.util.ssl_.DEFAULT_CIPHERS`, and a widely copied snippet edits it through Requests: `requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'`. It is the usual answer to a `dh key too small` handshake failure, because it stops the client offering Diffie-Hellman key exchange to a server with a weak group. urllib3 2.0 removed the string and lets OpenSSL choose its defaults. The `+=` line then raises the error in the title at import or startup, before any request is made. The fix has to do two things: remove the crash, and keep whatever the snippet was achieving for the one server that needed it.

bootmortis/iran-hosted-domains shows the usual layout. `src/utils.py` defines `fix_requests_ssl()`, which calls `disable_warnings()`, runs the `+=` line above, and then tries the same edit on `requests.packages.urllib3.contrib.pyopenssl.util.ssl_` inside `try` and `except AttributeError`. The guard covers only the second edit. `src/main.py` calls the function first thing under `if __name__ == "__main__":`, so a failure there stops the whole program. `requirements.txt` pins `requests==2.28.1`, and that release requires `urllib3<1.27`, so today's install resolves to a 1.26 release and works. The error is waiting in the next pull request that raises the Requests pin, which is why the agent treats this as part of the upgrade and not as a separate bug.

The agent confirms that sequence instead of assuming it. It installs the pinned requirements, checks that the attribute exists and prints its value, then installs the newest Requests in a second environment and reproduces the `AttributeError` with the project's own function. It also checks a variant that fails differently: code that assigns with `=` instead of `+=` raises nothing on urllib3 2, because assigning creates a new attribute that nothing reads. That version is worse, since the workaround disappears without any error, and the agent searches for both spellings.

The replacement keeps the setting off the global module. The agent writes a small `HTTPAdapter` subclass whose `init_poolmanager` passes `ssl_context=create_urllib3_context(ciphers=...)`, a function that accepts a cipher string in both urllib3 lines, and mounts it on a `requests.Session` for `https://`. The project calls `requests.get` and `requests.request` directly in `src/get_domians.py` and `src/utils.py`, so those calls move to the shared session, or the adapter would never be used. Before choosing the cipher string, the agent finds out which host needed the workaround. It connects to each URL the code fetches with the default context and records which handshakes fail, then mounts the adapter only for those hosts. If none fail, it says the workaround is no longer needed and proposes deleting it. It leaves `verify=False` in the `download` helper alone and lists it as a separate finding.

## Start it with one API call

`POST https://api.phoenix.so/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "This project raises \"AttributeError: module 'urllib3.util.ssl_' has no attribute 'DEFAULT_CIPHERS'\" with urllib3 2.x, or will as soon as requests is upgraded. Search the repository for DEFAULT_CIPHERS, for requests.packages.urllib3 and for urllib3.util.ssl_, and list every place that reads or modifies urllib3 module state, noting whether each uses += (which raises on urllib3 2) or plain assignment (which silently does nothing there). Read the dependency pins and say which urllib3 version the current lock or requirements resolve to and which requests version first allows urllib3 2. Reproduce both states in separate virtual environments: the pinned one, where the patch works, and the newest requests, where it fails, and show the output. Then replace the global patch with a requests HTTPAdapter subclass that builds its context with urllib3.util.ssl_.create_urllib3_context(ciphers=...) and passes it as ssl_context in init_poolmanager, mounted on a shared requests.Session. Route the existing requests.get, requests.post and requests.request calls through that session. Before picking the cipher string, connect to every host the code fetches using the default context and report which handshakes fail and with what error; mount the adapter only for hosts that need it, and if none do, propose removing the workaround. Add a test that builds the adapter, inspects the context's cipher list and asserts the intended suites are absent, with no network access. Do not change verify= settings; list any verify=False as a separate finding. Raise the requests pin in the same pull request only if the tests pass with it. Include commands and output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.phoenix.so/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

A pull request that deletes the global patch, adds the adapter and a session, and moves the existing calls onto that session. The description shows the two reproductions side by side: the pinned environment where the attribute exists, and the upgraded one with the traceback. It then gives a table of the hosts the code contacts, with the result of a default-context handshake for each, and says which hosts got the adapter. The test asserts on the cipher names in the adapter's context and needs no network.

A reviewer checks three things. The first is that no direct `requests.get` call is left outside the session, since such a call would skip the adapter without failing. The second is the host table: a workaround written years ago may protect a server that has since been fixed, and deleting it is better than porting it. The third is the pin. If the pull request raises Requests, the description says which urllib3 release that resolves to and that the suite ran against it. Findings that the agent did not act on, such as disabled certificate verification, are listed at the end for a separate decision.

## Run it on every Monday morning

This error arrives with a dependency bump, so the place to catch it is the weekly upgrade run. Add an automation on the Project with a `schedule` trigger, a cron such as `0 7 * * 1` and your time zone, and a prompt that upgrades the pinned requirements on a branch, runs the program's entry point and tests, and applies the fix above when the failure is this `AttributeError`. A schedule carries no repository event, so the saved prompt holds every instruction.

Pull requests opened by a dependency bot do not fire a `github` trigger, because bot senders never do. If you would sooner react to the bot's pull request than run on a schedule, have the CI job that fails on it send this request with an API key and a `request_id` that is a UUID v5 of the repository name and the commit SHA, so a re-run of the job returns the run it already started.

## Why run it on Rush?

The proof for this fix is a TLS handshake against real servers under two library versions, and both parts are awkward on a developer's machine. One virtual environment hides the other's behaviour, and a corporate proxy or a local OpenSSL build changes which handshakes succeed. The agent builds both environments from the repository's own pins in a clean sandbox, connects from there, and the result for each host is stored with the session. The host table in the pull request comes from that output.

The failure mode also argues for doing it carefully. Patching a module global affects every connection the process makes, and the plain-assignment variant stops working on upgrade without raising anything. A run that searches for both spellings, scopes the change to the hosts that need it and adds a test on the context turns an invisible setting into something a later upgrade will fail loudly on.

## Frequently asked questions

### Why did DEFAULT_CIPHERS disappear?

urllib3 2.0 stopped maintaining its own cipher list and uses the defaults of the OpenSSL it runs on, with TLS 1.2 as the minimum version. The constant was removed along with the list. Code that needs different ciphers now passes a context built with `create_urllib3_context(ciphers=...)`.

### Can I just pin urllib3<2 instead?

It works for now and is a reasonable stopgap, since the 1.26 line still has the attribute. It also keeps the process-wide patch and blocks Requests upgrades that need the new line. The agent can open that one-line pull request first if you ask, with the adapter as a follow-up.

### My code assigns DEFAULT_CIPHERS with = and nothing fails. Is it fine?

No. On urllib3 2 the assignment creates an attribute that the library never reads, so the cipher change has not been applied since the upgrade. Whether anything broke depends on whether the server still needs it, which the handshake check in this job answers.

### Does the adapter work on both urllib3 1.26 and 2.x?

Yes. `create_urllib3_context` takes a `ciphers` argument in both lines, and `HTTPAdapter.init_poolmanager` forwards extra keyword arguments such as `ssl_context` to the pool. That lets the fix merge before the pin is raised.
