Bug Report
- import path of package in question:
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob (and .../azblob/blockblob)
- SDK version:
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
- commit:
47b7a8a545174788f2bc750fdebda9bf4eb1e0ae (tag sdk/storage/azblob/v1.4.1)
- also reproducible on
main (see pointers below)
- output of
go version: go version go1.26.0 linux/amd64
What happened?
Client.UploadBuffer / Client.UploadFile accept an AccessConditions field via UploadBufferOptions / UploadFileOptions, but those access conditions are silently dropped when the payload exceeds MaxUploadBlobBytes (256 MiB) and the upload is performed as staged Put Block calls followed by a Put Block List commit.
As a result, a conditional create (IfNoneMatch: "*") is honored for payloads ≤ 256 MiB but not for payloads > 256 MiB — the existing blob is overwritten instead of the request failing.
Root cause is an asymmetry in blockblob/models.go. The single-shot path forwards AccessConditions, but the multi-block commit path used by UploadBuffer/UploadFile does not:
- The size branch in
uploadFromReader: ≤ MaxUploadBlobBytes → one Upload (Put Blob); otherwise stage blocks + CommitBlockList:
|
if actualSize <= MaxUploadBlobBytes { |
|
commitBlockListOptions := o.getCommitBlockListOptions() |
|
resp, err := bb.CommitBlockList(ctx, blockIDList, commitBlockListOptions) |
MaxUploadBlobBytes (256 MiB threshold):
|
// MaxUploadBlobBytes indicates the maximum number of bytes that can be sent in a call to Upload. |
|
MaxUploadBlobBytes = 256 * 1024 * 1024 // 256MB |
- Single-shot options do carry
AccessConditions (so the condition works ≤ 256 MiB):
|
func (o *uploadFromReaderOptions) getUploadBlockBlobOptions() *UploadOptions { |
|
return &UploadOptions{ |
|
Tags: o.Tags, |
|
Metadata: o.Metadata, |
|
Tier: o.AccessTier, |
|
HTTPHeaders: o.HTTPHeaders, |
|
AccessConditions: o.AccessConditions, |
|
CPKInfo: o.CPKInfo, |
|
CPKScopeInfo: o.CPKScopeInfo, |
|
} |
|
} |
- The bug —
uploadFromReaderOptions.getCommitBlockListOptions() builds CommitBlockListOptions with no AccessConditions field, so the entire AccessConditions (both ModifiedAccessConditions and LeaseAccessConditions) is discarded on the commit:
|
func (o *uploadFromReaderOptions) getCommitBlockListOptions() *CommitBlockListOptions { |
|
return &CommitBlockListOptions{ |
|
Tags: o.Tags, |
|
Metadata: o.Metadata, |
|
Tier: o.AccessTier, |
|
HTTPHeaders: o.HTTPHeaders, |
|
CPKInfo: o.CPKInfo, |
|
CPKScopeInfo: o.CPKScopeInfo, |
|
} |
|
} |
- For contrast, the sibling
UploadStreamOptions.getCommitBlockListOptions() does forward AccessConditions:
|
func (u *UploadStreamOptions) getCommitBlockListOptions() *CommitBlockListOptions { |
|
if u == nil { |
|
return nil |
|
} |
|
|
|
return &CommitBlockListOptions{ |
|
Tags: u.Tags, |
|
Metadata: u.Metadata, |
|
Tier: u.AccessTier, |
|
HTTPHeaders: u.HTTPHeaders, |
|
CPKInfo: u.CPKInfo, |
|
CPKScopeInfo: u.CPKScopeInfo, |
|
AccessConditions: u.AccessConditions, |
|
} |
|
} |
Still present on main: https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/storage/azblob/blockblob/models.go (uploadFromReaderOptions.getCommitBlockListOptions() still omits AccessConditions).
This is purely an SDK omission, not a service limitation — Put Block List supports conditional headers: https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list ("This operation also supports the use of conditional headers to commit the block list only if a specified condition is met.").
What did you expect or want to happen?
AccessConditions supplied to UploadBuffer/UploadFile should be applied to the final Put Block List commit — exactly as they already are for the single-shot Put Blob path and for UploadStream. With IfNoneMatch: "*", the second upload of an existing blob should fail (409 BlobAlreadyExists / 412 ConditionNotMet) and leave the blob unchanged, regardless of payload size. Either fix it, or document that high-level buffer/file uploads ignore access conditions above MaxUploadBlobBytes.
How can we reproduce it?
Upload the same blob twice with IfNoneMatch: "*". With a payload just over 256 MiB the second call succeeds and overwrites; with a payload ≤ 256 MiB it correctly fails.
package main
import (
"bytes"
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
)
func main() {
ctx := context.Background()
cred, _ := azblob.NewSharedKeyCredential("<account>", "<key>")
client, _ := azblob.NewClientWithSharedKeyCredential(
"https://<account>.blob.core.windows.net", cred, nil)
const container, name = "<container>", "conditional-create-test"
star := azcore.ETagAny
opts := &azblob.UploadBufferOptions{
AccessConditions: &blob.AccessConditions{
ModifiedAccessConditions: &blob.ModifiedAccessConditions{IfNoneMatch: &star},
},
}
// 260 MiB -> exceeds MaxUploadBlobBytes -> staged Put Block + Put Block List.
// Change to e.g. 1<<20 (1 MiB) and the second call correctly returns BlobAlreadyExists.
payload := bytes.Repeat([]byte{'A'}, 260*1024*1024)
_, err := client.UploadBuffer(ctx, container, name, payload, opts)
fmt.Println("first :", err) // <nil> (created)
_, err = client.UploadBuffer(ctx, container, name, payload, opts)
fmt.Println("second:", err,
"BlobAlreadyExists:", bloberror.HasCode(err, bloberror.BlobAlreadyExists))
// EXPECTED: err = 409 BlobAlreadyExists / 412 ConditionNotMet (blob NOT overwritten)
// ACTUAL : err = <nil>, the >256 MiB blob is silently overwritten
}
Observed with a small (8 MiB, single-shot) vs. large (260 MiB, multi-block) payload, each uploaded twice with a flipped content marker and overwrite confirmed via GetProperties (ETag) + a ranged read of the live content.
Anything we should know about your environment?
- Linux,
go1.26.0, Shared Key credential, standard azblob.Client.
- Threshold is
blockblob.MaxUploadBlobBytes (256 MiB): payloads at or below it take the single-shot Put Blob path (condition honored); above it take the multi-block Put Block + Put Block List path (condition dropped).
Bug Report
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob(and.../azblob/blockblob)github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.147b7a8a545174788f2bc750fdebda9bf4eb1e0ae(tagsdk/storage/azblob/v1.4.1)main(see pointers below)go version:go version go1.26.0 linux/amd64What happened?
Client.UploadBuffer/Client.UploadFileaccept anAccessConditionsfield viaUploadBufferOptions/UploadFileOptions, but those access conditions are silently dropped when the payload exceedsMaxUploadBlobBytes(256 MiB) and the upload is performed as stagedPut Blockcalls followed by aPut Block Listcommit.As a result, a conditional create (
IfNoneMatch: "*") is honored for payloads ≤ 256 MiB but not for payloads > 256 MiB — the existing blob is overwritten instead of the request failing.Root cause is an asymmetry in
blockblob/models.go. The single-shot path forwardsAccessConditions, but the multi-block commit path used byUploadBuffer/UploadFiledoes not:uploadFromReader: ≤MaxUploadBlobBytes→ oneUpload(Put Blob); otherwise stage blocks +CommitBlockList:azure-sdk-for-go/sdk/storage/azblob/blockblob/client.go
Line 442 in 47b7a8a
azure-sdk-for-go/sdk/storage/azblob/blockblob/client.go
Lines 520 to 521 in 47b7a8a
MaxUploadBlobBytes(256 MiB threshold):azure-sdk-for-go/sdk/storage/azblob/blockblob/constants.go
Lines 17 to 18 in 47b7a8a
AccessConditions(so the condition works ≤ 256 MiB):azure-sdk-for-go/sdk/storage/azblob/blockblob/models.go
Lines 292 to 302 in 47b7a8a
uploadFromReaderOptions.getCommitBlockListOptions()buildsCommitBlockListOptionswith noAccessConditionsfield, so the entireAccessConditions(bothModifiedAccessConditionsandLeaseAccessConditions) is discarded on the commit:azure-sdk-for-go/sdk/storage/azblob/blockblob/models.go
Lines 304 to 313 in 47b7a8a
UploadStreamOptions.getCommitBlockListOptions()does forwardAccessConditions:azure-sdk-for-go/sdk/storage/azblob/blockblob/models.go
Lines 361 to 375 in 47b7a8a
Still present on
main: https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/storage/azblob/blockblob/models.go (uploadFromReaderOptions.getCommitBlockListOptions()still omitsAccessConditions).This is purely an SDK omission, not a service limitation —
Put Block Listsupports conditional headers: https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list ("This operation also supports the use of conditional headers to commit the block list only if a specified condition is met.").What did you expect or want to happen?
AccessConditionssupplied toUploadBuffer/UploadFileshould be applied to the finalPut Block Listcommit — exactly as they already are for the single-shotPut Blobpath and forUploadStream. WithIfNoneMatch: "*", the second upload of an existing blob should fail (409 BlobAlreadyExists/412 ConditionNotMet) and leave the blob unchanged, regardless of payload size. Either fix it, or document that high-level buffer/file uploads ignore access conditions aboveMaxUploadBlobBytes.How can we reproduce it?
Upload the same blob twice with
IfNoneMatch: "*". With a payload just over 256 MiB the second call succeeds and overwrites; with a payload ≤ 256 MiB it correctly fails.Observed with a small (8 MiB, single-shot) vs. large (260 MiB, multi-block) payload, each uploaded twice with a flipped content marker and overwrite confirmed via
GetProperties(ETag) + a ranged read of the live content.Anything we should know about your environment?
go1.26.0, Shared Key credential, standardazblob.Client.blockblob.MaxUploadBlobBytes(256 MiB): payloads at or below it take the single-shotPut Blobpath (condition honored); above it take the multi-blockPut Block+Put Block Listpath (condition dropped).