Skip to content

Validate a caller-supplied extensions["timeout"] - #1189

Open
ebarkhordar wants to merge 2 commits into
pydantic:mainfrom
ebarkhordar:fix/1162-validate-timeout-extension
Open

Validate a caller-supplied extensions["timeout"]#1189
ebarkhordar wants to merge 2 commits into
pydantic:mainfrom
ebarkhordar:fix/1162-validate-timeout-extension

Conversation

@ebarkhordar

@ebarkhordar ebarkhordar commented Sep 4, 2026

Copy link
Copy Markdown

Summary

extensions["timeout"] reaches the transport unchanged and its values land in socket.settimeout(), so patching connect_tcp fixes the traceback in the issue but not the rest of it: put a Timeout in read or write and you get the same TypeError out of _backends/sync.py:126 and :137. I put the check where the caller's mapping is accepted instead, so it covers every key and both the build_request and send() paths.

I went with raising rather than coercing. Coercing means guessing which attribute was meant, and the getattr(timeout, "connect", None) version turns any other object into None, which is no timeout at all rather than an error. Can switch it if you'd prefer.

The guard accepts int, float and None, which is what socket.settimeout itself accepts, so nothing that works today starts failing (Decimal and Fraction already raise there). The extensions doc already shows this as a mapping of floats, so there was nothing to change there.

This leaves httpcore2 alone, so using httpcore2 directly still reaches the stdlib error. That is why it is Refs and not Fixes.

Refs #1162

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.

Review in cubic

The mapping reaches the transport unchanged and its values are handed to
`socket.settimeout()`, so a non-numeric value raised a stdlib TypeError from
inside the connect, read or write path rather than a clear error from httpx2.

Refs pydantic#1162
@codspeed-hq

codspeed-hq Bot commented Sep 4, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 7 skipped benchmarks1


Comparing ebarkhordar:fix/1162-validate-timeout-extension (f715034) with main (aaef581)

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.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/httpx2/httpx2/_client.py">

<violation number="1" location="src/httpx2/httpx2/_client.py:125">
P3: A boolean slips through the int/float check because `bool` subclasses `int`, so `extensions['timeout'] = {"read": True}` passes validation and becomes a 1-second timeout instead of raising. This contradicts the PR's goal of rejecting non-timeout values. Explicitly exclude `bool` from the accepted types.</violation>
</file>

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

Re-trigger cubic

"Use `Timeout(...).as_dict()` to build one."
)
for name, value in timeout.items():
if value is not None and not isinstance(value, (int, float)):

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.

P3: A boolean slips through the int/float check because bool subclasses int, so extensions['timeout'] = {"read": True} passes validation and becomes a 1-second timeout instead of raising. This contradicts the PR's goal of rejecting non-timeout values. Explicitly exclude bool from the accepted types.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/httpx2/httpx2/_client.py, line 125:

<comment>A boolean slips through the int/float check because `bool` subclasses `int`, so `extensions['timeout'] = {"read": True}` passes validation and becomes a 1-second timeout instead of raising. This contradicts the PR's goal of rejecting non-timeout values. Explicitly exclude `bool` from the accepted types.</comment>

<file context>
@@ -113,6 +113,19 @@ class UseClientDefault:
+            "Use `Timeout(...).as_dict()` to build one."
+        )
+    for name, value in timeout.items():
+        if value is not None and not isinstance(value, (int, float)):
+            raise TypeError(f"extensions['timeout'][{name!r}] must be a number or None, got {type(value).__name__}.")
+
</file context>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'd leave this one. The guard admits exactly what socket.settimeout admits, which is the point of it: nothing that works today starts raising. bool is in that set, and it is reachable without the extensions dict at all, since Timeout(5.0, read=True).as_dict() returns {'read': True, ...} and the value goes straight through. So excluding it here would only make the two paths disagree.

The behaviour is worse than a 1s read though, and that part is worth knowing: settimeout(False) sets the socket non-blocking rather than raising. Happy to file that separately against Timeout if you think it's worth rejecting bools on both paths.

Request shallow-copies extensions, so request.extensions["timeout"] is the
mapping the caller passed in and the equality was x == x. Snapshot it with
deepcopy first, which also pins that the new validator does not mutate its
argument.
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