Repository navigation
Do not follow HTTP redirects in the API and inference clients - #102
Merged
Merged
Conversation
InferenceClient, HTTPClient and the authentication token requests now send requests with allow_redirects=False. A 3xx response raises InferenceClientError or APIException. AsyncInferenceExecution.status_json() and result() now use the client timeout and raise on an HTTP error status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InferenceClient(follow_redirects=True) follows a redirect only if the target is inside the deployment: same scheme, host and port as endpoint_base_url, a path under the deployment path, and no relative path segment in the Location path. It follows at most 5 redirects. Any other redirect raises InferenceClientError. The default stays off. AsyncInferenceExecution status and result never follow a redirect. HTTPClient and the authentication calls do not change. When a redirect only adds or removes a trailing slash, the error message tells the caller to change path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ects-34af24 # Conflicts: # CHANGELOG.md # tests/unit_tests/inference_client/test_inference_client.py
With follow_redirects on, the part of the target path under the deployment must now pass check_request_path with the same allowlist as path. Forms such as `..;/`, `%`-escapes and `\` are rejected. The query string is not checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
yutkin
marked this pull request as ready for review
October 30, 2026 07:52
The example looks up a deployment and creates an InferenceClient with its endpoint_base_url and follow_redirects=True. Deployment.run_sync and run always use the default, so this is the way to turn on the option for a deployment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Point the service API pages at the public import paths, such as verda.containers.ContainersService. The old paths named modules that no longer exist, so autodoc could not import them. Include CHANGELOG.md and CONTRIBUTING.md with the recommonmark parser. The pages referenced .rst files that no longer exist, so they had no content and no title. Add inference_minimal_async to the container examples toctree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit 576cd40. The docs build fix goes in a separate pull request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Location that urllib cannot parse, such as an unclosed IPv6 bracket, raised ValueError from the trailing-slash hint or the redirect check. The redirect check now rejects such a Location, and the hint is empty. The client then raises InferenceClientError, as for any other redirect. Tests: assert APIException.code in the redirect tests, rename tests to end in _successful or _failed, and move module constants to the top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A response is now a redirect only if requests would follow it: 301, 302, 303, 307 or 308 with a Location header (Response.is_redirect). Before, every 3xx raised, so a 304 Not Modified became an error. Other 3xx responses go back to the caller, as on master. This applies to InferenceClient._send and to handle_error in the HTTP client. The InferenceClient verb helpers and health() passed timeout_seconds=None, so the request had no timeout. They now use the client timeout_seconds when the caller gives none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With follow_redirects on, the error said "the target is outside the deployment" also for a target inside the deployment that failed the path check. It now says that the target failed the follow_redirects check. CLAUDE.md said that has_relative_path_segment is for endpoint_base_url only, but the follow_redirects check also uses it on the raw Location path. The note now names both uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gateway returns Status "Error" for a failed job. AsyncStatus had no such value, so status_json() raised ValueError. AsyncStatus now has Error, and the async examples stop polling on it. For a failed job, /result/ returns the deployment's own error status and body. InferenceClientError dropped that body, so the caller could not see why the job failed. An HTTP error now ends with the response body, cut to 500 characters. result() compared Content-Type with 'application/json' exactly, so 'application/json; charset=utf-8' came back as text, and a missing header raised KeyError. It now compares the media type only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nicksemin
approved these changes
Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The SDK clients do not follow HTTP redirects by default. A redirect response raises an error. A redirect is a 301, 302, 303, 307 or 308 with a
Locationheader, the same rulerequestsuses. Other 3xx responses, such as 304, go back to the caller as before.InferenceClienthas a new opt-in argument,follow_redirects(defaultFalse). When it is on, the client follows a redirect only if all of these are true:endpoint_base_url.<deployment path>/.Locationpath has no relative segment, plain or encoded.path. This check rejects forms such as..;/,%-escapes and\. It does not check the query string.The client raises
InferenceClientErrorfor any other redirect.requestsmethod rules apply: 303 changes to GET, 301 and 302 change POST to GET, and 307 and 308 keep the method and body.status_json()andresult()never follow a redirect.HTTPClientand the authentication calls do not get the option.When a redirect only adds or removes a trailing slash, the error message tells you to change
path.A new example,
examples/containers/calling_the_endpoint_with_redirects.py, and a docs page,inference_redirects.rst, show the opt-in.Deployment.run_syncandrunalways use the default, so the example creates anInferenceClientwith the deployment'sendpoint_base_url.status_json()andresult()also use the clienttimeout_secondsnow. They raiseInferenceClientErroron a 4xx or 5xx. The gateway returns 200 from/status/for every job state, so pollingstatus_json()does not change. So doget,post,put,delete,patch,head,optionsandhealthwhen the caller gives notimeout_seconds. Before, those calls had no timeout.Related fixes for async jobs:
AsyncStatushas a new value,Error. Before,status_json()raisedValueErrorfor a failed job. The async examples now stop polling onError.InferenceClientErrorfor an HTTP error status ends with the response body, cut to 500 characters. For a failed job,/result/returns the deployment's error status and body, so the error shows why the job failed.result()parses JSON when theContent-Typehas parameters, such ascharset=utf-8. A result without aContent-Typeheader returns the text.Evidence
Unit test, for each client method and each status 301, 302, 303, 307 and 308:
/moved. The test fails.After: the client sends one request and raises
InferenceClientError. The test passes.Unit tests for
follow_redirects=True:..,%2e%2e,..%2F,..;/and..%252fsegmentspathdoes not allowhttpinstead ofhttps, another port, or user infoLocationthat the client cannot parsestatus_json()andresult()do not follow a redirect.Other unit tests:
Locationheader goes back to the caller, in both modes.health()send the clienttimeout_secondswhen the caller gives no timeout.status_json()returns a failed job withAsyncStatus.Error.result()for a failed job raises an error that ends with the deployment's body.result()parsesapplication/json; charset=utf-8, and returns text when there is noContent-Type.Tested locally:
uv run pytest tests/unit_tests: 660 passed, with security: check the inference request path with the API path rules #103 merged.uv run ruff checkanduv run ruff format --check: pass.Location. A request without a mismatch gets 200 before and after.Merge Danger
Door: two-way
A revert restores the old behavior.
Blast Radius: consumers
These cases can break:
http://base URL that the server redirects tohttps://. The call now raises an error.follow_redirects=Truedoes not help here, because the scheme changes.path. If the container sends aLocationinside the deployment,InferenceClient(..., follow_redirects=True)restores the old behavior.status_json()andresult()now raise on a 4xx or 5xx response. They also stop aftertimeout_seconds(default 300 seconds). Before the job completes,/result/returns 404, soresult()now raises. Before, it returned the 404 body as the result.InferenceClientverb helpers andhealth()now stop aftertimeout_seconds(default 300 seconds) when the caller gives no timeout. A call that waits longer than that for a response now raisesInferenceClientError.HTTPClientmethods ignoreallow_redirects=True.🤖 Generated with Claude Code