Skip to content

fix putObject never resolves when the input stream gets destroyed o… - #1480

Open
yucao2521 wants to merge 1 commit into
minio:masterfrom
yucao2521:fix/put-stream
Open

fix putObject never resolves when the input stream gets destroyed o…#1480
yucao2521 wants to merge 1 commit into
minio:masterfrom
yucao2521:fix/put-stream

Conversation

@yucao2521

@yucao2521 yucao2521 commented Jun 13, 2026

Copy link
Copy Markdown

fixes: #1479

Summary by CodeRabbit

  • Bug Fixes

    • Improved object uploads when using destroyed or non-readable streams, so requests fail with the correct underlying error (including clearer “Premature close” messaging).
  • Tests

    • Updated functional and unit tests to use promise-based assertions.
    • Expanded coverage to verify exact rejection messages for destroyed streams, including multipart upload cases.
  • Refactor

    • Improved multipart upload handling to more reliably skip already uploaded parts, abort cleanly on failure, and complete consistently.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR fixes a critical issue where putObject() with a destroyed input stream would hang indefinitely. The fix introduces stream error detection infrastructure, integrates error handling into putObject validation, replaces .pipe() with stream.pipeline() in multipart uploads for proper error propagation and cleanup, and adds comprehensive test coverage for stream error scenarios.

Changes

Stream Error Handling for putObject

Layer / File(s) Summary
Stream error detection infrastructure
src/internal/helper.ts
isReadableStream now detects streams via _read function presence instead of stream.isReadable(), and new getReadableStreamError exports an async helper that returns stream errors by awaiting streamPromise.finished on non-active streams.
putObject stream validation
src/internal/client.ts
Import and use getReadableStreamError to detect input stream errors in putObject, replacing generic type error check with stream-specific validation that throws actual stream errors when present.
Multipart upload coordination with pipeline
src/internal/client.ts
Refactor uploadStream to use streamPromise.pipeline(body, chunkier) for proper error forwarding, add async part-upload loop that iterates chunks with MD5 comparison for resumption, uploads missing parts with Content-MD5, accumulates part ETags, and aborts multipart upload on failure.
Test coverage for stream errors
tests/unit/test.js, tests/functional/functional-tests.js
Unit tests updated to expect "Premature close" for destroyed streams and add case for custom stream errors; functional tests refactored to async/await with Promise rejection assertions covering immediate destruction and multipart upload destruction scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops through streaming trails,
Where promises once broke and failed,
Now pipeline flows with graceful care,
And errors caught beyond compare! 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix putObject never resolves when the input stream gets destroyed' clearly summarizes the main bug fix addressed in the PR, accurately reflecting the core issue from #1479.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from #1479: it uses stream.pipeline() for proper error handling, implements stream error detection with getReadableStreamError(), and handles destroyed stream scenarios with appropriate error propagation.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the putObject stream destruction issue: helper refactoring, client implementation updates, and test updates are all scoped to resolving the linked issue requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/internal/client.ts`:
- Around line 1802-1849: The async uploader awaits the pipeline and the consumer
separately which can deadlock if makeRequestAsyncOmit rejects inside the
consumer (o); change the try block to await both the pipeline and the consumer
together (e.g., await Promise.all([streamPromise.pipeline(body, chunkier), o]))
so the pipeline and the async iterator (o) are observed as one unit and ensure
abortMultipartUpload is still called on any rejection from either side; keep
references to chunkier, o, makeRequestAsyncOmit, streamPromise.pipeline and
abortMultipartUpload when making the change.
- Around line 1808-1840: The bug is that partNumber is incremented before
uploading the current chunk, causing chunk N to be uploaded as part N+1; fix by
keeping the current chunk on the current partNumber: do not increment partNumber
until after you have either skipped a matching oldPart (the existing branch
where you compare oldPart.etag === md5.toString('hex') should increment and
continue) or after a successful upload and pushing to eTags; specifically,
remove the premature partNumber++ that appears immediately before the upload
RequestOption, and instead increment partNumber only after eTags.push({ part:
partNumber, etag }) (and keep the existing increment where you skip matched
oldPart).

In `@tests/unit/test.js`:
- Around line 670-674: The test creates a Readable stream `s` and calls
`s.destroy(new Error('stream error'))` without an 'error' listener which can
cause an unhandled exception; before calling `s.destroy(...)` add an empty error
handler like `s.on('error', () => {})` (i.e., update the test around the
`it('should fail when stream is destroyed with an error', ...)` block so the
`Stream.Readable` instance `s` has `s.on('error', () => {})` attached prior to
calling `client.putObject('bucket', 'object', s)` and `s.destroy(...)`).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cbae5fd8-c249-4ba8-910e-9df28e9a463a

📥 Commits

Reviewing files that changed from the base of the PR and between f871afd and 9f01f3e.

📒 Files selected for processing (4)
  • src/internal/client.ts
  • src/internal/helper.ts
  • tests/functional/functional-tests.js
  • tests/unit/test.js

Comment thread src/internal/client.ts Outdated
Comment thread src/internal/client.ts
Comment thread tests/unit/test.js

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/internal/client.ts`:
- Around line 1688-1692: The code currently only checks for destroy errors via
getReadableStreamError(stream) but accepts streams that have already been
consumed without error. Since getReadableStreamError() returns undefined for
both "still open" and "finished cleanly" states, you need to add an additional
check after the error probe to reject exhausted streams. After checking if an
error exists from getReadableStreamError(), also verify that the stream is not
already finished or exhausted. If the stream is in a finished/exhausted state
(meaning it was already consumed and returned no error), throw an error to
prevent uploading empty data when the caller provided a positive size. This
ensures that only viable streams proceed past this validation point.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e4f2da02-ff3a-4313-a8fe-d8fdc0d8fb5a

📥 Commits

Reviewing files that changed from the base of the PR and between 9f01f3e and 55172c4.

📒 Files selected for processing (4)
  • src/internal/client.ts
  • src/internal/helper.ts
  • tests/functional/functional-tests.js
  • tests/unit/test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/test.js
  • tests/functional/functional-tests.js

Comment thread src/internal/client.ts Outdated
@prakashsvmx

Copy link
Copy Markdown
Member

could you address the comments @yucao2521 ?

@yucao2521

Copy link
Copy Markdown
Author

Hi @prakashsvmx , I've resolved the comments from CodeRabbit. Please have a review. Thanks.

BR,
Yu

@prakashsvmx

Copy link
Copy Markdown
Member

Thank you,
Please check the ci reported test failure @yucao2521

@yucao2521
yucao2521 force-pushed the fix/put-stream branch 2 times, most recently from 56db184 to 874ce60 Compare June 22, 2026 18:49
@yucao2521

Copy link
Copy Markdown
Author

Ah, sorry. It's fixed now, please check again. Thanks, @prakashsvmx .

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

Fixes an issue where putObject() could remain unsettled when an input Readable stream is destroyed, by improving stream error detection and multipart upload stream wiring, and updates tests to assert the new rejection behavior.

Changes:

  • Add readable stream error introspection (getReadableStreamError) and use it in putObject() to fail fast on destroyed/non-readable streams.
  • Refactor multipart upload stream handling to use pipeline() and abort multipart uploads on failure.
  • Update unit/functional tests to assert specific rejection messages for destroyed streams (including multipart scenarios).

Reviewed changes

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

File Description
src/internal/helper.ts Adds getReadableStreamError() and relaxes isReadableStream() detection to allow destroyed streams to be handled via finished().
src/internal/client.ts Uses getReadableStreamError() in putObject(); refactors multipart upload flow to use pipeline() and abort on error.
tests/unit/test.js Updates/extends unit tests to expect “Premature close” or the explicit destroy error message.
tests/functional/functional-tests.js Converts destroyed-stream functional steps to promise-based rejection assertions and expands coverage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 38 to 40
const assert = chai.assert
const expect = chai.expect

Comment thread src/internal/client.ts
Comment on lines +1811 to +1818
const oldPart = oldParts[partNumber]
if (oldPart) {
if (oldPart.etag === md5.toString('hex')) {
eTags.push({ part: partNumber, etag: oldPart.etag })
partNumber++
continue
}
}
Comment thread src/internal/client.ts
Comment on lines +1845 to +1850
})().catch((err) => {
if (!chunkier.closed) {
chunkier.destroy(err)
}
throw err
})
Comment thread src/internal/client.ts
Comment on lines +1854 to +1857
} catch (err) {
await this.abortMultipartUpload(bucketName, objectName, uploadId)
throw err
}
@prakashsvmx

Copy link
Copy Markdown
Member

could you please check and validate the review comments @yucao2521 ?

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.

putObject() never resolves or rejects if the input stream gets destroyed on upload

3 participants