Implement Put Blob From URL - #2749
Conversation
There was a problem hiding this comment.
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
putBlobFromUrlimplementation toBlockBlobHandler, including property/metadata/tag handling and source-condition enforcement via a sharedreadCopySource()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.
3963f70 to
e8f065d
Compare
There was a problem hiding this comment.
🟡 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
There was a problem hiding this comment.
🟡 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
Writewhen the destination exists, but it doesn't enforceCreatewhen the destination does not exist. SinceOperationAccountSASPermission.validatePermissions()is also an "any-of" check, a token with onlywcan create a new blob viaPutBlobFromUrl, 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
There was a problem hiding this comment.
🔵 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
There was a problem hiding this comment.
🔵 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-shapedCannotVerifyCopySourceerror. 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
There was a problem hiding this comment.
🔵 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
|
Andrew Gaul (@gaul) , |
|
Akanksha Jain (@jainakanksha-msft) Added in 6951d71. Put Blob From URL now accepts The swagger gains the Tests cover the returned header, a matching value, a mismatch, a short value, and CRC64 combined with either |
There was a problem hiding this comment.
🟢 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
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
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
6951d71 to
5725668
Compare
There was a problem hiding this comment.
🔵 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
| 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 () => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| ]; | ||
| const expectedContentMD5 = requestMD5s.find((md5) => md5 !== undefined); | ||
| const expectedContentCRC64 = options.transactionalContentCrc64; | ||
| if ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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
There was a problem hiding this comment.
🔵 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
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.