Repository navigation
Conversation
`writable_stream` had no `acl` parameter, so no form of
`S3File.writer()` sent `x-amz-acl`. It now takes the ACL and gives it
to the upload.
`writer()` without an options object sent no `x-amz-storage-class`. It
now sends the storage class that the file has, as `writer({})` does.
This was referenced Sep 30, 2026
Collaborator
Author
|
Status: draft. It waits for #43140. How I reproduced it: |
Collaborator
Author
|
Closing: #44287 now has this change. It is one pull request for the writer options and for |
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.
Stacked on #44287. Do not merge before #43140.
Problem
S3File.writer()never sendsx-amz-acland, without arguments, nox-amz-storage-class.write()sends both. The object gets the default ACL and class, or a bucket policy that requires the ACL refuses the upload.writable_stream(src/runtime/webcore/s3/client.rs:410) has noaclparameter.get_writerwithout an options object (src/runtime/webcore/Blob.rs:1651) passesNonefor the storage class.Fix
writable_streamtakesacland gives it to theMultiPartUpload, asupload_streamdoes.get_writerpasses theaclandstorage_classof the store, or the parsedaclwhen there is an options object.MultiPartUploadalready sends both fields on PutObject and on CreateMultipartUpload.test/js/bun/s3/s3-requester-pays.test.ts(35 new cases, 31 fail on the base). Self-reviewed: 29 concerns raised, 28 addressed (Notes).Background
aclandstorage_classon its store (store::S3,src/jsc/webcore_types.rs:828).writer()reports an upload failure through a pendingflush()orend(). A failure with no pending promise is lost (S3File.writer() drops an upload failure that arrives with no pending flush() or end() promise: end() resolves 0 #43133). s3: report a writer() upload failure that no pending promise could carry #43140 reports it.Downsides
writer()with an ACL or storage class that the store refuses now fails, aswrite()does (for example 400AccessControlListNotSupported). Before, the object was stored without them.end()resolves 0, nothing is stored. Main stores the object.writer()(996, base 989),.text+0 B.Notes
Why this is a draft. With this change a store can refuse a
writer()upload that it accepted before, because the upload now carries the header.writer()loses a failure that arrives when noflush()orend()is pending (#43133). The loop indocs/guides/write-file/stream.mdx(for await (const chunk of stream) writer.write(chunk), thenend()) is that shape. #43140 reports the failure. It does not compile on main today: it callsNetworkSink::path(), which #41688 removed. When #43140 is on main I add the case below as a test and mark this PR ready.A bucket that has ACLs disabled (
BucketOwnerEnforcedintest/packages/s3-server), a writer withacl: "public-read"andretry: 0:end()resolves 1, stored, no header sentend()rejectsAccessControlListNotSupported, nothing stored, 1 requestend()at onceAccessControlListNotSupported, nothing stored, 1 requestawait flush()flush()rejectsAccessControlListNotSupported, nothing storedend()after the answer to CreateMultipartUploadacl: "private"or"bucket-owner-full-control", 1 bytewrite(data)with the same optionsAccessControlListNotSupportedThe fourth row is the one that #43140 changes. The same loss occurs on main with no ACL: a writer with a wrong
secretAccessKeyand 5 MiB + 1 resolves 0 and stores nothing. A bucket withBlockPublicAclsanswers 403AccessDeniedin place of the 400.A bucket policy that requires the ACL (
Deny s3:PutObjectunlesss3:x-amz-aclisbucket-owner-full-control), 1 byte:file(key, { acl }).writer()andwriter({ acl })end()rejectsAccessDeniedafter 4 PutObjectfile(key, { acl }).write(data)Measurements. Release builds from one toolchain: #44287 (the base of this PR) and this branch. The counts are from gdb, inside the dynamic extent of
get_writer, on the thread that entered it.writer()without options: allocations / frees / parser calls / credential copies (mode, 191 of 200 calls)writer({})andwriter({ acl }): allocations / frees / parser calls / clone / dupe (mode, 191 of 200 calls)writer(bag)/writer()writer()without options (gdbstepi, 10th call, JIT off, 12 runs).textofbun(llvm-size -A)get_writer/writable_stream(nm -S)The
writer(bag)row is from the merge base of #44287, which has the same code for that branch. valgrind and bloaty are not installed in the container, and apt reaches no mirror. The instruction count is from gdb for that reason.The test.
writer(options). Each case comparesx-amz-acl,x-amz-storage-classandx-amz-request-payer. The test server refuses a request with anx-amz-*header that is not signed, so the cases do not compareSignedHeaders.flush()case callsend()afterflush(). Without it nothing holds the writer, the garbage collector takes it, and the upload fails withUnknownError.BUN_JSC_validateExceptionChecks=1,BUN_DESTRUCT_VM_ON_EXIT=1and LeakSanitizer: 70 pass, 0 leak records.s3.test.ts(307 pass, 270 skipped without R2 credentials), the other files oftest/js/bun/s3/,test/regression/issue/s3-header-injection.test.ts. I ran them with--timeout 120000because of the load of the host.Self-review. Rejected: expected-failure rows for the sibling sites (code this PR does not change). Not carried from the review: a list of S3-compatible services that refuse one of the two headers. I had no way to check it from this environment.
Not in this PR. #44289 has the first two, with output.
contentDispositionandcontentEncodinggiven tofile()or to the client reach no upload, fromwrite()or fromwriter(). The store has no field for them.presign()without options does not use the ACL and the storage class of the file. A comment atS3File.rs:563says that this is deliberate.writer(bag)readstype,contentDispositionandcontentEncodingtwo times. To remove the first read changes the text of an error.options,acl,storage_classandrequest_payergo toupload,writable_streamandupload_streamas separate positional arguments. One struct for them is a possible follow-up.Open PRs near these lines. #40252 moves the body of
writable_streamintonew_upload(..., acl, ...)and passesNonefrom the writer. After this PR that call passesacl. #43045 changes the signed headers, which these tests do not compare.Earlier work. There is no user report for
writer(). #33502 made this change inside a larger one. It went stale and got no review.Question.
get_writerkeeps its two calls ofwritable_stream. One call behind the test for the options object is possible at the same cost per call. I kept the two calls because the diff is 7 lines. Say so if you prefer the one call.