Skip to content

MLE-32970 Prevent retries of multipart streaming requests - #1979

Open
rjdew-progress wants to merge 3 commits into
developfrom
MLE-32953
Open

rjdew-progress wants to merge 3 commits into
developfrom
MLE-32953

Conversation

@rjdew-progress

Copy link
Copy Markdown
Contributor

Why

A Data Services request with streaming multipart input can fail with Stream closed after a preceding gzipped Row Manager response. The IOException retry interceptor sees only the outer MultipartBody, so it can retry the request even though a nested StreamingOutputImpl has already consumed and closed its source.

Approach

  • Mark StreamingOutputImpl as one-shot using OkHttp's native RequestBody.isOneShot() contract.
  • Make the IOException retry interceptor honor isOneShot() in addition to the Java Client's RetryableRequestBody contract.
  • Add a regression test proving that one-shot status propagates through MultipartBody and prevents a second request attempt.

Validation

  • ./gradlew marklogic-client-api:test --tests com.marklogic.client.impl.StreamingOutputImplTest
  • The complete core test task was also attempted; its server-dependent tests could not run because no deployed MarkLogic test instance was available on localhost:8012.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Multipart bodies wrapping InputStreamHandle may still be replayed because one-shot status is not propagated through ObjectRequestBody.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Prevents retries of multipart requests containing one-shot streaming bodies.

Changes:

  • Marks StreamingOutputImpl as one-shot.
  • Honors OkHttp’s isOneShot() contract in retry handling.
  • Adds multipart streaming regression coverage.
File Reviewed change
marklogic-client-api/​src/​test/​java/​com/​marklogic/​client/​impl/​StreamingOutputImplTest.java Tests one-shot multipart behavior.
marklogic-client-api/​src/​main/​java/​com/​marklogic/​client/​impl/​StreamingOutputImpl.java Declares streaming bodies as one-shot.
marklogic-client-api/​src/​main/​java/​com/​marklogic/​client/​impl/​okhttp/​RetryIOExceptionInterceptor.java Avoids retries for one-shot request bodies.

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

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

All reviewed changes are covered and no unresolved blocking issues were identified.

Review effort: Lite
Findings: None

Resolved since last review (1)

@rjdew-progress rjdew-progress changed the title MLE-32953 Prevent retries of multipart streaming requests MLE-32970 Prevent retries of multipart streaming requests Sep 24, 2026
…ndability

StreamingOutputImpl previously hardcoded isRetryable()=false / isOneShot()=true
for every wrapped OutputStreamSender, even when the originating write handle
was fully buffered and resendable (e.g. JacksonHandle, StringHandle). This
meant any request body built from such a handle -- including Row Manager plan
execution, document writes, and Data Services parameters -- was needlessly
blocked from being retried by RetryIOExceptionInterceptor after a transient
connection error, even though the content could safely be written again.

Thread each call site's already-known isResendable()/handleBase.isResendable()
value into StreamingOutputImpl instead of assuming non-resendable:
- writeDocumentImpl-style document writes
- putPostValueImpl
- postResource / putResource (used by RowManagerImpl for Optic plan execution)
- postIteratedResourceImpl (multipart-mixed eval/invoke style resources)
- addParts multipart builder
- makeRequestBodyForContent and BaseProxy's makeRequestBody (Data Services
  single-node parameters)

Call sites without an existing resendable concept (structured query search,
alert match) keep the prior conservative false/one-shot default via new 3-arg
doPost/doPut overloads, so behavior is unchanged where resendability was
never determined.

Added StreamingOutputImplTest cases covering: a resendable body being retried
after a simulated connection failure, and a non-resendable part inside a
MultipartBody still propagating isOneShot()=true so it is never retried.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Java client always sends Accept-Encoding: gzip, and MarkLogic can close a connection after replying to such a request even though OkHttp keeps it pooled. A Data Services call that streams a one-shot body (e.g. a Reader), made right after another call such as RowManager.resultRows(), could be written to that closed connection. The body was consumed before the failure was noticed, so the retry failed with 'Stream closed'.

The ping that guards against this only ran for the first request on a thread, and only for digest authentication, so it had already been used up by the earlier call. It now runs before every request that has a streaming part, so a closed connection is found by a ping that has no body to lose.

This relies on StreamingOutputImpl reporting one-shot bodies as such: with the ping but without that, a failure after the ping is resent by OkHttp and reports 'Stream closed' again.

StreamingPartRequestTest (mock server) checks that the ping happens before each streaming request and that a failed streaming request is not resent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

3 participants