Skip to content

AsyncHttpClient does not verify TLS certificates by default (sync HttpClient does) #7511

Description

@toolshedlabs-hash

Summary

On chromadb 1.5.9, chromadb.AsyncHttpClient(..., ssl=True) does not verify the server
certificate. The sync HttpClient, with the same settings, does.

Settings.chroma_server_ssl_verify defaults to None (chromadb/config.py:146). The two clients
read that default differently.

Sync, chromadb/api/fastapi.py:90 on main (:85 in the 1.5.9 wheel):

if self._settings.chroma_server_ssl_verify is not None:
    self._session = httpx.Client(timeout=None, limits=self.http_limits,
                                 verify=self._settings.chroma_server_ssl_verify)
else:
    self._session = httpx.Client(timeout=None, limits=self.http_limits)

Async, chromadb/api/async_fastapi.py:144 on main (:139 in the wheel):

self._clients[loop_hash] = httpx.AsyncClient(
    timeout=None,
    headers=headers,
    verify=self._settings.chroma_server_ssl_verify or False,
    limits=self.http_limits,
)

None or False is False, and verify=False in httpx turns off chain and hostname checking. So
the async client accepts any certificate that is offered, with no error and no warning.

Reproduction

chromadb 1.5.9, Python 3.14.0, fresh venv. The listener is a plain TLS socket holding a
self-signed cert for CN=localhost, addressed as 127.0.0.1 so the name does not match either. It
counts completed handshakes. Nothing leaves the machine.

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3 -nodes -subj "/CN=localhost"
await chromadb.AsyncHttpClient(host="127.0.0.1", port=P, ssl=True)                       # A
chromadb.HttpClient(host="127.0.0.1", port=P, ssl=True)                                  # B
await chromadb.AsyncHttpClient(host="127.0.0.1", port=P, ssl=True,
                               settings=Settings(chroma_server_ssl_verify=True))         # C
case TLS handshakes completed outcome
A, async, default settings 1 handshake succeeded against an untrusted cert
B, sync, default settings 0 SSLCertVerificationError
C, async, ssl_verify=True 0 SSLCertVerificationError

Read the handshake column. Row A then fails on the fake origin's response shape, which is not part
of the claim. Row B is what proves the certificate really is untrusted. Row C is what proves the
setting still works when it is truthy, so this is about the default and not about verification
being broken everywhere.

When it changed

The commit history on that file puts it at commit 22e7f84, PR #3236 "[BUG] Add headers to async client
requests", 2024-12-03. The whole diff is:

-            self._clients[loop_hash] = httpx.AsyncClient(timeout=None)
+            self._clients[loop_hash] = httpx.AsyncClient(
+                timeout=None,
+                headers=self._settings.chroma_server_headers,
+                verify=self._settings.chroma_server_ssl_verify or False,
+            )

Before that the async client had no verify argument at all, so it used the httpx default and
verified. I checked five earlier commits on the file and none of them mention ssl_verify. The PR
was about headers, so the or False looks incidental rather than intended.

Suggested fix

Match the sync branch:

kwargs = {"timeout": None, "headers": headers, "limits": self.http_limits}
if self._settings.chroma_server_ssl_verify is not None:
    kwargs["verify"] = self._settings.chroma_server_ssl_verify
self._clients[loop_hash] = httpx.AsyncClient(**kwargs)

That keeps chroma_server_ssl_verify=False working for people who want to skip verification, and
keeps a CA bundle path working, while restoring the default.

Happy to open a PR with a test that asserts the async client refuses a self-signed cert by default,
if that is useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions