Skip to content

SSLError - #1156

Open
ccbest wants to merge 5 commits into
pydantic:mainfrom
ccbest:ssl-error
Open

SSLError#1156
ccbest wants to merge 5 commits into
pydantic:mainfrom
ccbest:ssl-error

Conversation

@ccbest

@ccbest ccbest commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds SSLError to both httpcore2 and httpx2, raised when a TLS handshake fails, per issue #854. Also fixes a related bug where a failed handshake on the trio backend surfaced with no error message at all.

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

Why ConnectError instead of RequestError

  1. The exception mapper would first match to NetworkError and ConnectError, so SSLError would never actually surface if it directly inherited from RequestError. The alternative would cause a breaking change as ConnectError, NetworkError, and TransportError would stop catching TLS failures.

  2. Inheriting from ConnectError mirrors requests.exceptions.SSLError (subclasses ConnectionError) and aiohttp.ClientSSLError (subclasses ClientConnectorError) behavior.

trio Error Message Fix

trio reports a failed handshake as trio.BrokenResourceError that stringifies to "". A trio certificate error would surface as ConnectError('') with no description to indicate the failure was TLS related.

Comparison I tested -

Backend Error
sync ConnectError "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate"
asyncio ConnectError "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate"
trio ConnectError ""

After this PR all three backends report identically.

Follow up:

I noticed that retries= will also retry a TLS handshake failure, which is consistent with urllib3's behavior but is probably not optimal. This PR would enable httpx2 to shortcut a certificate failure by defining a further CertificateError subclass while allowing transient SSL errors (SSLEOFError, TLSV1_ALERT_INTERNAL_ERROR) to retry. I opened a corresponding discussion in #1157 .

AI Disclosure: Claude Opus 5 assisted in drafting this PR; all claims and every line of code was verified by a human (myself)

Review in cubic

ccbest added 4 commits August 22, 2026 08:22
Adds `SSLError` to both `httpcore2` and `httpx2`, raised when a TLS
handshake fails. It subclasses `ConnectError`, so code that already
catches `ConnectError` is unaffected, while code that wants to
distinguish a TLS failure from a TCP failure now can.

This also brings the hierarchy in line with the libraries the change is
meant to ease migration from: `requests.exceptions.SSLError` subclasses
`ConnectionError`, and `aiohttp.ClientSSLError` subclasses
`ClientConnectorError`.

On the `trio` backend a failed handshake arrives wrapped in a
`trio.BrokenResourceError`, which carries no message of its own, so the
error previously surfaced as a `ConnectError` with an empty string. The
underlying `ssl.SSLError` is now recovered from `__cause__`, which both
types the error correctly and restores the message.

Closes pydantic#854
The other exception classes in `httpcore2._exceptions` are bare `pass`,
and httpcore2 docstrings are not rendered anywhere in the docs. The
rationale for the parent class lives on the `httpx2` counterpart, which
is rendered.
Re-raising trio's own `ssl.SSLError` created a reference cycle: trio sets
`BrokenResourceError.__cause__` to the ssl error, so `raise cause from exc`
pointed the ssl error back at the `BrokenResourceError`. Code walking
`__cause__ or __context__` — a common pattern in logging and error
reporting — would not terminate.

Raise a new `SSLError` carrying the original message instead. The chain
is now acyclic and in causal order:

    httpx2.SSLError -> httpcore2.SSLError -> trio.BrokenResourceError
        -> ssl.SSLCertVerificationError -> None
@codspeed-hq

codspeed-hq Bot commented Aug 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 7 skipped benchmarks1


Comparing ccbest:ssl-error (f001430) with main (5465b4e)

Open in CodSpeed

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/httpx2/httpx2/_exceptions.py
Comment thread tests/httpx2/test_exceptions.py Outdated
…Error and change test assertions to look for lower-case reason text
@ccbest

ccbest commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

CICD appears to be failing due to an unrelated issue with websockets in testing. The previous run passed all six python versions - the latest commit with the failures doesn't touch any websocket code and I wasn't able to reproduce the failure locally.

Could a maintainer re-run the failed jobs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant