Skip to content

fix(python): do not replay POST requests after a server error - #1353

Draft
pbxqdown wants to merge 1 commit into
kubernetes-sigs:mainfrom
pbxqdown:fix/python-post-retry
Draft

fix(python): do not replay POST requests after a server error#1353
pbxqdown wants to merge 1 commit into
kubernetes-sigs:mainfrom
pbxqdown:fix/python-post-retry

Conversation

@pbxqdown

@pbxqdown pbxqdown commented Aug 8, 2026

Copy link
Copy Markdown

What this PR does / why we need it:

commands.run() sends POST /execute, which runs a shell command inside the
sandbox. Both Python connectors retried that request on 500/502/503/504, so a
command whose response failed after the runtime had already run it was executed
again — six attempts in total. The caller saw a single SandboxRequestError
with no indication that the command had run more than once.

A retryable status code means the connection succeeded and the request body
reached the server, so the work it describes may already be done. This change
limits transport-level replay to GET, PUT and DELETE: the sync connector
drops POST from the urllib3 Retry(allowed_methods=...), and the async
connector checks the method alongside the status code.

Concretely, for a POST:

Failure mode Before After
Server answers 5xx replayed not replayed
Body sent, no response (read error, sync connector) replayed not replayed
Never connected (dial error) unchanged unchanged

The read-error row changes for the same reason as the 5xx row: the body reached
the server, so the command may already be running. Dial-time failures are
untouched — urllib3's allowed_methods does not gate connect-error retries, and
the async connector never retried transport errors to begin with.

This matches how the rest of the project draws the line. Run() in the Go SDK
defaults to a single attempt, and the router retries dial-class failures only
because replaying a body that may already have been sent could duplicate side
effects.

Testing: two regression tests were added, both failing before this change and
passing after — test_connector.py::TestSandboxConnectorStrategySelection::test_post_requests_are_not_retried
and test_async_sandboxclient.py::TestAsyncConnector::test_post_requests_are_not_retried_on_server_error.
make test-unit passes. The behaviour was also verified end to end against a
stub runtime that performs a side effect and then answers 503: it records the
command executed 6 times on main and once with this change, for both the sync
and async connectors.

Which issue(s) this PR is related to:

Fixes #1352

Release Note

Fixed the Python SDK re-executing sandbox commands after a transient server error.

@github-project-automation github-project-automation Bot moved this to Backlog in Agent Sandbox Aug 8, 2026
@kubernetes-prow kubernetes-prow Bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. do-not-merge/invalid-commit-message Indicates that a PR should not merge because it has an invalid commit message. labels Aug 8, 2026
@netlify

netlify Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploy Preview for agent-sandbox canceled.

Name Link
🔨 Latest commit 6a1bb64
🔍 Latest deploy log https://app.netlify.com/projects/agent-sandbox/deploys/6a76a675aa29e20008ee1f84

@kubernetes-prow
kubernetes-prow Bot requested review from janetkuo and vicentefb August 8, 2026 03:32
@kubernetes-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: pbxqdown
Once this PR has been reviewed and has the lgtm label, please assign justinsb for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🚫 Review skipped — only excluded labels are configured. (3)
  • needs-ok-to-test
  • do-not-merge/work-in-progress
  • cncf-cla: no

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ad275273-7098-4f21-bfe5-cf06aa1646b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kubernetes-prow kubernetes-prow Bot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Aug 8, 2026
@kubernetes-prow

Copy link
Copy Markdown

Welcome @pbxqdown!

It looks like this is your first PR to kubernetes-sigs/agent-sandbox 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-sigs/agent-sandbox has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@kubernetes-prow kubernetes-prow Bot added the needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. label Aug 8, 2026
@kubernetes-prow

Copy link
Copy Markdown

Hi @pbxqdown. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@kubernetes-prow kubernetes-prow Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Aug 8, 2026
POST /execute runs a shell command inside the sandbox, so a 5xx response
can arrive after the runtime already ran the command. Both connectors
retried it, executing the command up to six times while the caller saw a
single error. Limit transport-level retries to GET, PUT and DELETE.
@pbxqdown
pbxqdown force-pushed the fix/python-post-retry branch from 4f367fd to 6a1bb64 Compare August 8, 2026 03:45
@kubernetes-prow kubernetes-prow Bot removed the do-not-merge/invalid-commit-message Indicates that a PR should not merge because it has an invalid commit message. label Aug 8, 2026
@janetkuo
janetkuo requested a lite review from Copilot August 8, 2026 04:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Python SDK’s retry behavior to avoid replaying side-effecting POST requests (notably command execution) after transient 5xx responses, aligning the SDK with the project’s “don’t replay commands” reliability expectations.

Changes:

  • Sync connector: removes POST from the urllib3 Retry(allowed_methods=...) set.
  • Async connector: retries 5xx responses only for GET/PUT/DELETE, not POST.
  • Adds regression tests covering both sync and async connectors to ensure POST is not retried on 5xx responses.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
clients/python/agentic-sandbox-client/k8s_agent_sandbox/connector.py Narrows urllib3 retryable HTTP methods to avoid replaying POST after 5xx.
clients/python/agentic-sandbox-client/k8s_agent_sandbox/async_connector.py Adds method check so 5xx retries only happen for non-POST methods.
clients/python/agentic-sandbox-client/k8s_agent_sandbox/test/unit/test_connector.py Adds sync regression test asserting POST is not in retryable methods.
clients/python/agentic-sandbox-client/k8s_agent_sandbox/test/unit/test_async_sandboxclient.py Adds async regression test asserting POST is not retried on 5xx.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 223 to 225
delay = BACKOFF_FACTOR * (2 ** attempt)
logger.warning(
f"Retryable status {response.status_code} from {url}, "

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.

Thanks for looking at this. I don't think the response is left open here.

AsyncClient.request() calls send(..., stream=False), which awaits response.aread(). Reading a response to completion runs aiter_raw() to the end, whose last statement is await self.aclose(); Response.aclose() is documented as "Automatically called if the response body is read to completion". So the connection is already released back to the pool before the loop continues, and an explicit close would early-return on is_closed.

This loop is also unchanged by this PR apart from the added method check. Happy to add an explicit close anyway if you would rather see it stated.

Comment on lines 312 to +316
retries = Retry(
total=5,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE"]
allowed_methods=RETRYABLE_METHODS,

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.

Good thing to check, but allowed_methods does not gate connect-time retries.

In urllib3 2.7 Retry.increment(), the connection-error branch decrements connect without consulting _is_method_retryable(). Only the read-error branch and is_retry() (status retries) consult it.

Measured with this connector's own Retry policy, counting POST attempts that reach the transport:

POST failure mode POST in allowed_methods POST removed
nothing listening (dial) 6 6
server reads the request, then closes (read) 6 1

So dial-time failures behave exactly as before, which is what the description meant.

The read case does change, and that is intended: the body reached the server, so the command may already be running — the same risk as the 5xx case this PR is about. The description only mentioned 5xx, so I have made that explicit.

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

Labels

action-required: resolve-copilot-comments cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

Python SDK re-executes sandbox commands after a transient 5xx

3 participants