Skip to content

fix(client): retry only replayable request content - #3771

Open
Hughhhhcoder wants to merge 2 commits into
openai:mainfrom
Hughhhhcoder:codex/openai-python-retry-content
Open

fix(client): retry only replayable request content#3771
Hughhhhcoder wants to merge 2 commits into
openai:mainfrom
Hughhhhcoder:codex/openai-python-retry-content

Conversation

@Hughhhhcoder

@Hughhhhcoder Hughhhhcoder commented Aug 31, 2026

Copy link
Copy Markdown
  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

Raw streaming request bodies are currently reused across automatic retries. A one-shot iterable is exhausted after the first attempt, so a 500, timeout, or connection error can be followed by an empty retry. If that empty retry succeeds, the SDK reports success even though it did not resend the payload. Seekable file-like content has the same issue because its position is not restored.

This change keeps retry behavior correct without eagerly buffering large request bodies:

  • bytes, bytearrays, and exact list/tuple containers continue to retry normally
  • seekable file-like content is restored to its original offset before each retry
  • opaque sync and async iterables are treated conservatively as non-replayable, including wrappers that return one stored iterator
  • multipart file handling remains out of scope and is already covered by Fix multipart uploads on retry #3484

The regression tests cover retryable status responses, timeouts, and connection errors for sync and async one-shot content, compatibility for exact list/tuple containers, plus original-offset restoration for seekable content.

Additional context & links

The original regression cases fail on main and pass after this change.

Validation:

  • pytest tests/test_client.py: 213 passed, 2 skipped (Pydantic v2)
  • pytest tests/test_client.py: 213 passed, 2 skipped (Pydantic v1)
  • full ./scripts/lint: pass (Ruff, formatting, Pyright, mypy, and import checks)
  • Castiron custom-code budget: pass (6566 / 10000)

@Hughhhhcoder
Hughhhhcoder requested a review from a team as a code owner August 31, 2026 12:59
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T02:51:58.641408Z 53256c2 Manual request
🔒 Security Review Completed 2026-09-01T02:50:47.377617Z 53256c2 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sylvesterkaczmarek

Copy link
Copy Markdown

A one-shot Iterable can still be misclassified as replayable here. isinstance(content, Iterator) only catches objects that implement __next__; a valid Iterable[bytes] can instead return the same internal iterator from __iter__() without being an Iterator itself.

After the first request consumes that internal iterator, rewind() returns True, and the retry reuses the exhausted iterable, so it can still send an empty body and report success.

Could we either treat generic iterables conservatively unless repeatability is known, or add a regression case with an iterable wrapper whose __iter__() returns one stored generator? That reproduces the silent empty-retry case this PR is intended to prevent.

@Hughhhhcoder

Copy link
Copy Markdown
Author

@sylvesterkaczmarek Good catch — the stored-generator wrapper reproduced the silent empty-success retry. I addressed it in 53256c2 by treating opaque sync and async iterables as non-replayable unless their repeatability is known here; exact list/tuple containers remain retryable. The parameterized regression now covers stored sync/async iterators across status, timeout, and connection retries. Both Pydantic v1 and v2 client suites pass (213 passed, 2 skipped each), and the full lint/type-check suite passes.\n\n@codex review\n@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 53256c2765

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 53256c2765

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Hughhhhcoder

Copy link
Copy Markdown
Author

Thanks for checking this edge case. I rechecked the current head (53256c2765): the helper no longer uses isinstance(content, Iterator) to decide replayability. It conservatively permits only exact list/tuple containers (type(content) in (list, tuple)), so an Iterable wrapper that returns one stored generator is treated as non-replayable.

The regression already includes both synchronous and asynchronous one-shot wrappers whose __iter__/__aiter__ return the same stored iterator, parameterized across status, timeout, and connection failures. The focused retry suite passes (15 tests). I also confirmed the current head does not retry the wrapper, so no additional code change is needed for this feedback.

@sylvesterkaczmarek sylvesterkaczmarek 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.

_RequestContentReplay only checks options.content; multipart options.files is ignored. A non-seekable file upload has content=None, so the client still retries after the first attempt consumes the file and can send an empty/partial upload. Include file parts in replayability/rewind logic, or disable retries when any upload part is non-seekable, and add a multipart regression.

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.

2 participants