Skip to content

Implement Put Blob From URL - #2749

Open
Andrew Gaul (gaul) wants to merge 9 commits into
Azure:mainfrom
gaul:put-blob-from-url
Open

Implement Put Blob From URL#2749
Andrew Gaul (gaul) wants to merge 9 commits into
Azure:mainfrom
gaul:put-blob-from-url

Conversation

@gaul

Copy link
Copy Markdown
Contributor

Put Blob From URL answered 501, so a client copying a blob in one request had to fall back to the asynchronous Copy Blob and then wait out a copy it had asked to have done by the time it was answered. Read the source the way Put Block From URL already does -- a loopback self-request pinned to the address and port this server is bound to, carrying the caller's path, query and source conditions -- and commit what comes back as a new block blob. The two operations now share that fetch, so authentication, conditions, and the refusal to decompress a source that merely declares an encoding are answered the same way for both, in one place.

What the destination ends up with follows the operation's contract. The source's standard properties are copied unless x-ms-copy-source-blob-properties says otherwise, and a blob content header on the request sets that one property either way. Metadata answers to its own rule: named on the request it replaces the source's, named nowhere it is copied. Tags are the request's, or the source's under x-ms-copy-source-tag-option: COPY, which reads them over the same authorized path the content came over -- the extra Get Blob Tags call against the source that the operation is documented to make, so a URL allowed to read the source but not its tags is refused rather than obeyed. Asking for both at once is refused as it already is for Copy Blob From URL.

The operation writes a blob, so it joins Put Blob in the shared access signature tables and in the rule that an existing destination demands write permission; an operation missing from those tables fails the request with an internal error rather than a permission one.

References #2681.

Copilot AI lite review requested due to automatic review settings August 25, 2026 16:11

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.

Pull request overview

Implements synchronous Put Blob From URL support for block blobs in Azurite by reusing the existing “loopback self-request” copy-source fetch pattern (previously used for Put Block From URL), ensuring authentication and source conditions are enforced through the standard download path.

Changes:

  • Add putBlobFromUrl implementation to BlockBlobHandler, including property/metadata/tag handling and source-condition enforcement via a shared readCopySource() helper.
  • Extend SAS permission mappings and “existing destination requires write” logic to include BlockBlob_PutBlobFromUrl.
  • Add a comprehensive test suite for Put Blob From URL behavior and update documentation/changelog to reflect support.

Reviewed changes

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

Show a summary per file
File Description
tests/blob/apis/blockblob.test.ts Adds end-to-end tests for Put Blob From URL covering overwrite, headers/properties, metadata, tags, tiers, conditions, and MD5 behaviors.
src/blob/handlers/BlockBlobHandler.ts Implements Put Blob From URL and refactors shared loopback source-fetch logic used by both putBlobFromUrl and stageBlockFromURL.
src/blob/authentication/OperationBlobSASPermission.ts Registers required blob/container SAS permissions for the new Put Blob From URL operation.
src/blob/authentication/OperationAccountSASPermission.ts Registers account SAS permission requirements for Put Blob From URL.
src/blob/authentication/BlobSASAuthenticator.ts Includes Put Blob From URL in the “if destination exists, must have write” special-case rule.
src/blob/authentication/AccountSASAuthenticator.ts Includes Put Blob From URL in the “if destination exists, must have write” special-case rule for account SAS.
README.md Moves Put Blob From URL into the supported API matrix with the “same Azurite instance only” limitation.
ChangeLog.md Documents the new Put Blob From URL support and its behavioral details.

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

Comment thread ChangeLog.md Outdated

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.

🟡 Changes recommended

putBlobFromUrl currently ignores transactionalContentMD5 (Content-MD5) from the generated spec, so provided checksums (including invalid-length values) are not validated as expected.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/blob/handlers/BlockBlobHandler.ts Outdated
Copilot AI review requested due to automatic review settings September 7, 2026 18:18

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.

🟡 Changes recommended

SAS authorization currently doesn’t enforce Create permission when the destination blob does not exist, allowing PutBlobFromUrl creation with only write permission.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

src/blob/authentication/AccountSASAuthenticator.ts:263

  • Account SAS has the same gap as Blob SAS: the special-case block enforces Write when the destination exists, but it doesn't enforce Create when the destination does not exist. Since OperationAccountSASPermission.validatePermissions() is also an "any-of" check, a token with only w can create a new blob via PutBlobFromUrl, which is more permissive than Azure's intended permission model.
    // If copy destination blob exists, then permission must be Write only
    if (
      operation === Operation.BlockBlob_Upload ||
      operation === Operation.BlockBlob_PutBlobFromUrl ||
      operation === Operation.PageBlob_Create ||
      operation === Operation.AppendBlob_Create ||
      operation === Operation.Blob_StartCopyFromURL ||
      operation === Operation.Blob_CopyFromURL
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/blob/authentication/BlobSASAuthenticator.ts
Copilot AI review requested due to automatic review settings September 7, 2026 18:34

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.

🔵 Needs a closer look

It adds a large new Blob API implementation plus auth/permission surface changes, which should receive final human review despite strong test coverage.

Review details

Suppressed comments (1)

src/blob/authentication/OperationBlobSASPermission.ts:537

  • The comment for the container SAS permission mapping says "must be write", but this operation (like Put Blob) permits either Create or Write to create a new blob; only overwriting an existing blob is restricted to Write-only via the authenticator special-case. Updating the comment avoids encoding the wrong semantics.
  Operation.BlockBlob_PutBlobFromUrl,
  // Create a new blob, must be write
  new OperationBlobSASPermission(
    BlobSASPermission.Write + BlobSASPermission.Create
  )
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/blob/authentication/OperationAccountSASPermission.ts
Comment thread src/blob/authentication/OperationBlobSASPermission.ts
Copilot AI review requested due to automatic review settings September 7, 2026 18:51

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.

🔵 Needs a closer look

The new tag-fetch path (readCopySourceTags) does not currently map mid-stream/parse failures to an Azure-shaped CannotVerifyCopySource error, which can leak unhandled exceptions as generic 500s.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/blob/handlers/BlockBlobHandler.ts:1145

  • readCopySourceTags() reads and parses the loopback response stream without mapping mid-stream/socket failures or XML parse errors to an Azure-shaped CannotVerifyCopySource error. If the connection resets after headers, or the body is truncated, this will currently surface as an unhandled exception (generic 500) instead of the same error contract used elsewhere in Put/Stage From URL flows.
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 7, 2026 18:59

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.

🔵 Needs a closer look

It introduces a new Blob API surface plus shared loopback fetch/auth behavior, so a final human review is needed to confirm protocol compatibility and operational safety across environments.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@jainakanksha-msft

Copy link
Copy Markdown
Member

Andrew Gaul (@gaul) ,
x-ms-content-crc64 is neither accepted nor returned. The REST contract requires validation when supplied and says successful responses always include computed CRC64. The handler only selects MD5 inputs and computes MD5. The checked-in Swagger also omits CRC64 for this operation, so generated models/mappers/specification need updating too.
could you please add necessary handling for crc64 as well.

@jainakanksha-msft Akanksha Jain (jainakanksha-msft) linked an issue Sep 8, 2026 that may be closed by this pull request
Copilot AI review requested due to automatic review settings September 8, 2026 16:11
@gaul

Copy link
Copy Markdown
Contributor Author

Akanksha Jain (@jainakanksha-msft) Added in 6951d71.

Put Blob From URL now accepts x-ms-content-crc64 and checks it against the copied content: a mismatch is rejected as Crc64Mismatch, a value shorter than 8 bytes as InvalidHeaderValue, and a CRC64 sent alongside an MD5 header as BothCrc64AndMd5HeaderPresent, as Put Blob does. Every successful response carries the CRC64 computed over the copied content.

The swagger gains the ContentCrc64 parameter and the x-ms-content-crc64 201 header for BlockBlob_PutBlobFromUrl, and the generated models, mapper and operation spec were updated to match. The response header mirrors the upstream client swagger; the request parameter is not in the client swagger, so it is recorded as change 18 in swagger/blob.md.

Tests cover the returned header, a matching value, a mismatch, a short value, and CRC64 combined with either Content-MD5 or x-ms-source-content-md5.

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.

🟢 Approval recommended

The implementation is service-compatible in approach (loopback source enforcement), updates SAS/auth tables appropriately, and adds comprehensive blob + SAS test coverage for the new behavior.

Review details
  • Files reviewed: 11/14 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Andrew Gaul (gaul) and others added 4 commits September 8, 2026 09:21
Put Blob From URL answered 501, so a client copying a blob in one
request had to fall back to the asynchronous Copy Blob and then wait
out a copy it had asked to have done by the time it was answered.  Read
the source the way Put Block From URL already does -- a loopback
self-request pinned to the address and port this server is bound to,
carrying the caller's path, query and source conditions -- and commit
what comes back as a new block blob.  The two operations now share that
fetch, so authentication, conditions, and the refusal to decompress a
source that merely declares an encoding are answered the same way for
both, in one place.

What the destination ends up with follows the operation's contract.
The source's standard properties are copied unless
x-ms-copy-source-blob-properties says otherwise, and a blob content
header on the request sets that one property either way.  Metadata
answers to its own rule: named on the request it replaces the source's,
named nowhere it is copied.  Tags are the request's, or the source's
under x-ms-copy-source-tag-option: COPY, which reads them over the same
authorized path the content came over -- the extra Get Blob Tags call
against the source that the operation is documented to make, so a URL
allowed to read the source but not its tags is refused rather than
obeyed.  Asking for both at once is refused as it already is for Copy
Blob From URL.

The operation writes a blob, so it joins Put Blob in the shared access
signature tables and in the rule that an existing destination demands
write permission; an operation missing from those tables fails the
request with an internal error rather than a permission one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Put Blob From URL compared the copied content with x-ms-source-content-md5
or x-ms-blob-content-md5 but ignored Content-MD5, which the swagger carries
for the operation and which Put Blob, whose semantics the operation follows
for these headers, validates.  Compare it too, ranked below the other two
as Put Blob ranks it below x-ms-blob-content-md5, and reject a malformed
value in any of the three before the source is fetched rather than only in
the one that would have been compared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7wFx5XWMdmqezhwP3YvZQ
Azure grants Put Blob From URL on a new block blob to Create or Write and
on an existing one to Write alone, the split Put Blob has, and the
authenticators already answer it that way: the permission table accepts
either, and an existing destination then demands Write.  A review read
the table's any-of check as letting Write create a blob it should not,
which is the documented behaviour, so pin all four cells of the table for
account and blob SAS rather than change them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7wFx5XWMdmqezhwP3YvZQ
The comments on the operation's permission-table entries were copied from
Put Blob's and read as though Create alone could create the blob, or
Write alone had to.  Either creates it; only overwriting an existing blob
takes Write alone, which the authenticators check separately.  Say that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7wFx5XWMdmqezhwP3YvZQ
Andrew Gaul (gaul) and others added 2 commits September 8, 2026 09:21
Put Blob From URL maps a copy source body that fails midway to
CannotVerifyCopySource, but the tag read it makes under
x-ms-copy-source-tag-option: COPY read and parsed its body bare, so a
reset after the headers or a body that does not parse escaped as a
bodiless 500.  Give it the same answer, and release the stream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7wFx5XWMdmqezhwP3YvZQ
The REST reference lists x-ms-content-crc64 among this operation's
request headers and says the response always carries the CRC64 the
service computed. The checked-in swagger had neither, so the request
header was never deserialized and the response never carried one.

Add the ContentCrc64 parameter and the x-ms-content-crc64 response
header to the operation's swagger entry, mirror them into the generated
models, mapper and operation spec, and have the handler compare a
supplied value with the copied content, reject one sent alongside an
MD5 as Put Blob does, and report the computed CRC64 on every success.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgrmeBoe86wCnWC8vXR4D4

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.

🔵 Needs a closer look

It introduces a large, behavior-sensitive handler implementation and refactors shared copy-source fetch paths, so final validation by a human reviewer (plus CI signal) is warranted despite strong test additions.

Review details
  • Files reviewed: 11/14 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Comment thread tests/blob/sas.test.ts Outdated
await blob2SAS.syncCopyFromURL(blob1.url);
});

it("Put blob from URL with write permission in blob SAS should create and override a blob @loki @sql", async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Andrew Gaul (@gaul) These tests are named "blob SAS," but they construct the credential with containerClient.generateSasUrl() and ContainerSASPermissions, so they exercise a container-scoped SAS (sr=c). Could we rename them accordingly and add true blob-scoped coverage using blockBlobClient.generateSasUrl() with BlobSASPermissions? Please also add a negative creation case for each independent authorization path: account SAS, container SAS, and blob SAS with neither c nor w (for example, r only) should fail with 403 AuthorizationPermissionMismatch. The current tests cover c/w success and c-only overwrite denial, but do not pin rejection when neither required permission is present.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The container-signed tests are now named for the container SAS, blob-scoped tests using blockBlobClient.generateSasUrl with BlobSASPermissions sit beside them, and each of account, container and blob SAS has a read-only negative asserting 403 AuthorizationPermissionMismatch.

Comment thread src/blob/handlers/BlockBlobHandler.ts Outdated
];
const expectedContentMD5 = requestMD5s.find((md5) => md5 !== undefined);
const expectedContentCRC64 = options.transactionalContentCrc64;
if (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Andrew Gaul (@gaul) Could we move this pre-fetch checksum-header validation into the shared checksum utilities? computeAndValidateTransactionalChecksums() already owns the selected MD5/CRC64 mutual-exclusion and shape checks, while this block and stageBlockFromURL() maintain additional versions of the same rules. A synchronous helper could validate all supplied MD5 candidates plus CRC64 (accepting the caller-specific CRC64 header name), return the selected expected pair, and be reused by the compute helper and both URL handlers. That would preserve early rejection before fetching the source while keeping error semantics in one place. This is a maintainability suggestion rather than a functional blocker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. validateTransactionalChecksumHeaders in utils.ts now owns the mutual-exclusion and shape checks. It takes the MD5 candidates in precedence order and the CRC64 header name to report, and is called by computeAndValidateTransactionalChecksums, putBlobFromUrl and stageBlockFromURL, so early rejection stays and the rules live in one place.

// COPY reads the source's tags over the same authorized path the content
// came over, so a source that the caller may read but not tag refuses
// the copy rather than leaking them.
const blobTags = copySourceTags

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Andrew Gaul (@gaul) Azure requires the caller to satisfy Set Blob Tags authorization when x-ms-tags sets destination tags, and x-ms-copy-source-tag-option: COPY likewise requires Set Blob Tags permission on the destination. This handler persists those tags, but the new account/blob/container SAS mappings only require c or w, so a destination SAS without t can currently write tags. Could we conditionally require t when this operation sets or copies destination tags, with account-, container-, and blob-scoped SAS tests? Requests that do not write tags should continue to require only the documented c/w permissions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Both SAS authenticators now require Tag for this operation when x-ms-tags is present or x-ms-copy-source-tag-option is COPY, and leave requests that set no tags to Create or Write. Tests cover account, container and blob SAS. Note the same doc sentence applies to Put Blob and Copy Blob From URL, which Azurite does not enforce today; I left those as they were, since changing them is outside this PR.

Andrew Gaul (gaul) and others added 3 commits September 10, 2026 10:39
Put Blob From URL and Put Block From URL each kept their own copy of the
shape checks the shared validator makes, so that a malformed header is
rejected before the source is fetched rather than after it has been read.
Move those checks into validateTransactionalChecksumHeaders, which takes
the MD5 candidates in precedence order and the name of the CRC64 header
to report, and have the shared validator and both handlers call it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7wFx5XWMdmqezhwP3YvZQ
The tests named for a blob SAS signed the container, so name them for
what they sign and add the blob-scoped pair beside them. Also pin that a
SAS with neither Create nor Write is refused at each of the three
scopes, which the create and overwrite cases alone left open.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7wFx5XWMdmqezhwP3YvZQ
Azure requires a request that sets tags with x-ms-tags, or copies the
source's with x-ms-copy-source-tag-option: COPY, to satisfy Set Blob
Tags authorization on the destination on top of the write. Check that
in the account and blob SAS authenticators for this operation, so a SAS
without Tag is refused with AuthorizationPermissionMismatch, and leave
requests that set no tags to Create or Write alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7wFx5XWMdmqezhwP3YvZQ
Copilot AI review requested due to automatic review settings September 10, 2026 17:41

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.

🔵 Needs a closer look

It introduces a new Blob API surface with security-sensitive loopback fetching and multiple intertwined contract behaviors (SAS/auth, conditions, tags, and checksum semantics) that warrants final human review despite strong test coverage.

Review details
  • Files reviewed: 13/16 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Put Blob From URL

3 participants