Skip to content

Conversation

@JamieMagee
Copy link
Contributor

@JamieMagee JamieMagee commented Jan 20, 2026

Summary

Migrate from the deprecated azure-storage package to the modern Azure SDK packages:

  • @azure/storage-blob for blob storage
  • @azure/storage-queue for queue storage

Changes

  • Replace callback-based API with native Promises
  • Use async iterators for blob listing instead of continuation tokens
  • Remove manual promisify() wrappers (new SDK is Promise-native)
  • Remove withFilter() retry policy (built into new SDK)
  • Update tests to mock new SDK interfaces

References

This is part of a series of PRs to address deprecated packages in the project.

Stack

@JamieMagee JamieMagee changed the title chore(deps): migrate from azure-storage to @azure/storage-* SDK chore(deps): migrate from azure-storage to @azure/storage-* SDK Jan 20, 2026
@JamieMagee JamieMagee requested a review from Copilot January 20, 2026 21:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates from the deprecated azure-storage package to the modern Azure SDK packages (@azure/storage-blob v12.30.0 and @azure/storage-queue v12.29.0). The migration replaces callback-based APIs with native Promises, uses async iterators for blob listing, and updates error handling to use HTTP status codes instead of error codes.

Changes:

  • Replace azure-storage with @azure/storage-blob and @azure/storage-queue packages
  • Convert all blob and queue operations from callback-based to Promise-based async/await patterns
  • Update error handling to check error.statusCode instead of error.code for 404 checks
  • Implement async iterators for blob listing operations replacing continuation token-based pagination
  • Update all test mocks to reflect new SDK interfaces with proper async iterator and stream handling

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
package.json Adds @azure/storage-blob and @azure/storage-queue dependencies, removes azure-storage, adds xml2js
package-lock.json Updates dependency tree with new Azure SDK packages and their dependencies
providers/stores/abstractAzblobStore.js Migrates base store class to use BlobServiceClient with Promise-based APIs and async iterators
providers/stores/azblobHarvestStore.js Updates harvest store with async iterators and Promise-based blob operations
providers/stores/azblobDefinitionStore.js Migrates definition store to use new SDK with updated error handling (statusCode vs code)
providers/stores/azblobAttachmentStore.js Updates attachment store with new blob client and stream-to-string helper
providers/queueing/azureStorageQueue.js Migrates queue service to QueueServiceClient with receiveMessages API
test/providers/store/azblobHarvest.js Updates test mocks to simulate new SDK's containerClient and async iterators
test/providers/store/azblobDefinition.js Refactors test mocks for new blob client methods with proper async patterns
test/providers/store/azblobAttachment.js Updates attachment store tests with new getBlobClient mock interface

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

…zure/storage-queue

Replace deprecated azure-storage package with the modern Azure SDK:
- @azure/storage-blob for blob storage operations
- @azure/storage-queue for queue operations

The new SDK uses native Promises, async iterators, and built-in retry policies.
@JamieMagee JamieMagee force-pushed the jamiemagee/migrate-azure-storage-sdk branch from 1cef4ef to 12a753d Compare January 23, 2026 04:45
Copy link
Contributor Author

@JamieMagee JamieMagee left a comment

Choose a reason for hiding this comment

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

Found a potential bug with double base64 encoding in the queue implementation.

Comment on lines 25 to 28
async queue(message) {
await promisify(this.queueService.createMessage).bind(this.queueService)(this.options.queueName, message)
// The new SDK expects base64 encoded messages by default for compatibility
const encodedMessage = Buffer.from(message).toString('base64')
await this.queueClient.sendMessage(encodedMessage)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This manually base64-encodes the message before calling sendMessage(), but the new @azure/storage-queue SDK accepts plain strings and handles encoding internally. The old azure-storage SDK worked the same way.

This causes double-encoding:

  1. Manual base64 encoding here
  2. Azure Queue Storage encodes the message again when storing it

During a rolling deployment where old and new code versions run simultaneously, messages queued by the new code will be double-encoded. Old consumers will decode once and get base64 gibberish instead of valid JSON.

The fix is to remove the manual encoding:

async queue(message) {
  await this.queueClient.sendMessage(message)
}

The new @azure/storage-queue SDK does not automatically base64-encode
messages, so we should not decode them either. Messages are now sent
and received as plain JSON strings.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comments suppressed due to low confidence (1)

providers/queueing/azureStorageQueue.js:68

  • Similar to the dequeue() method, this code doesn't handle base64-encoded messages from the old SDK. If there are existing messages in the queue that were encoded with the old SDK, they won't be properly decoded. Consider adding backward compatibility to handle both base64-encoded (old) and plain text (new) messages.
      if (message.dequeueCount <= 5) {
        results.push({
          original: message,
          data: JSON.parse(message.messageText)
        })

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

Comment on lines +38 to +40
downloadResponse.readableStreamBody.pipe(stream)
downloadResponse.readableStreamBody.on('end', () => resolve(downloadResponse))
downloadResponse.readableStreamBody.on('error', reject)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The stream piping logic doesn't handle the 'close' event of the destination stream. If the destination stream is closed prematurely (e.g., due to an error on the write side), this could lead to unhandled promise rejections or resource leaks. Consider adding an event handler for the 'close' event on the destination stream to properly clean up and reject the promise if the stream closes unexpectedly.

Suggested change
downloadResponse.readableStreamBody.pipe(stream)
downloadResponse.readableStreamBody.on('end', () => resolve(downloadResponse))
downloadResponse.readableStreamBody.on('error', reject)
const source = downloadResponse.readableStreamBody
const dest = stream
let settled = false
const cleanup = () => {
source.removeListener('end', onSourceEnd)
source.removeListener('error', onSourceError)
if (dest) {
dest.removeListener('error', onDestError)
dest.removeListener('close', onDestClose)
}
}
const onSourceEnd = () => {
if (settled) return
settled = true
cleanup()
resolve(downloadResponse)
}
const onSourceError = err => {
if (settled) return
settled = true
cleanup()
reject(err)
}
const onDestError = err => {
if (settled) return
settled = true
cleanup()
reject(err)
}
const onDestClose = () => {
if (settled) return
settled = true
cleanup()
reject(new Error('Destination stream closed before source stream ended'))
}
source.on('end', onSourceEnd)
source.on('error', onSourceError)
if (dest) {
dest.on('error', onDestError)
dest.on('close', onDestClose)
source.pipe(dest)
} else {
// If no destination stream is provided, just drain the source
source.on('data', () => {})
}

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +46
if (message.dequeueCount <= 5) {
return {
original: message,
data: JSON.parse(message.messageText)
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The old azure-storage SDK automatically base64-encoded queue messages, but the new @azure/storage-queue SDK does not. The code now directly parses message.messageText without base64 decoding. This is a breaking change if there are existing messages in the queue that were encoded with the old SDK, as they won't be decodable. Consider adding backward compatibility to handle both base64-encoded (old) and plain text (new) messages during the migration period, or ensure all queues are drained before deploying this change.

This issue also appears in the following locations of the same file:

  • line 64

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +59
async _streamToString(readableStream) {
/** @type {Buffer[]} */
const chunks = []
for await (const chunk of readableStream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
return Buffer.concat(chunks).toString('utf8')
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The _streamToString helper method is duplicated in this file and in abstractAzblobStore.js. Since AzBlobAttachmentStore doesn't extend AbstractAzBlobStore, this duplication is necessary. However, consider extracting this into a shared utility module to maintain DRY principles and ensure consistent behavior across all storage implementations.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to 49
const response = await this.queueClient.receiveMessages({ numberOfMessages: 1 })
if (!response.receivedMessageItems || response.receivedMessageItems.length === 0) return null

const message = response.receivedMessageItems[0]
if (message.dequeueCount <= 5) {
return {
original: message,
data: JSON.parse(message.messageText)
}
}
await this.delete({ original: message })
return this.dequeue()
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The recursive call to dequeue() when a message has exceeded the dequeue count could potentially cause a stack overflow if there are many consecutive messages with high dequeue counts. While unlikely in practice, consider using a loop instead of recursion to handle this scenario more safely.

Suggested change
const response = await this.queueClient.receiveMessages({ numberOfMessages: 1 })
if (!response.receivedMessageItems || response.receivedMessageItems.length === 0) return null
const message = response.receivedMessageItems[0]
if (message.dequeueCount <= 5) {
return {
original: message,
data: JSON.parse(message.messageText)
}
}
await this.delete({ original: message })
return this.dequeue()
// Use a loop instead of recursion to avoid potential stack overflows when
// many consecutive messages exceed the dequeueCount threshold.
while (true) {
const response = await this.queueClient.receiveMessages({ numberOfMessages: 1 })
if (!response.receivedMessageItems || response.receivedMessageItems.length === 0) return null
const message = response.receivedMessageItems[0]
if (message.dequeueCount <= 5) {
return {
original: message,
data: JSON.parse(message.messageText)
}
}
await this.delete({ original: message })
}

Copilot uses AI. Check for mistakes.
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.

2 participants