Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

## Upcoming Release


General:

- Bump mysql2 to resolve to 3.10.1 for security patches
Expand All @@ -13,7 +12,7 @@ Blob:

- Fixed issue of download 0 size blob with range > 0 should report error. (issue #2410)
- Fixed issue of download a blob range without header x-ms-range-get-content-md5, should not return content-md5. (issue #2409)

- Fixed issue of blob batch request handling failing when request boundary includes '='. (issue #2413)

## 2024.06 Version 3.31.0

Expand Down
120 changes: 86 additions & 34 deletions src/blob/handlers/BlobBatchHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,17 @@ import ContainerHandler from "./ContainerHandler";
import PageBlobHandler from "./PageBlobHandler";
import PageBlobRangesManager from "./PageBlobRangesManager";
import ServiceHandler from "./ServiceHandler";
import uuid from "uuid";

type SubRequestNextFunction = (err?: any) => void;
type SubRequestHandler = (req: IRequest, res: IResponse, locals: any, next: SubRequestNextFunction) => any;
type SubRequestErrorHandler = (err: any, req: IRequest, res: IResponse, locals: any, next: SubRequestNextFunction) => any;

export interface BatchResponse {
contentType: string;
reponseBody: string;
}

export class BlobBatchHandler {
private handlers: IHandlers;
private authenticators: IAuthenticator[];
Expand Down Expand Up @@ -451,52 +457,95 @@ export class BlobBatchHandler {

public async submitBatch(
body: NodeJS.ReadableStream,
requestBatchBoundary: string,
subRequestPathPrefix: string,
batchRequest: IRequest,
context: Context
): Promise<string> {
const perRequestPrefix = `--${requestBatchBoundary}${HTTP_LINE_ENDING}`;
const batchRequestEnding = `--${requestBatchBoundary}--`

const requestBody = await this.requestBodyToString(body);
let subRequests: BlobBatchSubRequest[] | undefined;
): Promise<BatchResponse> {
let error: any | undefined;
try {
subRequests = await this.parseSubRequests(
const subResponses: BlobBatchSubResponse[] = [];
let subRequests: BlobBatchSubRequest[] | undefined;

const responseBatchBoundary = `batchresponse_${uuid()}`;
const perResponsePrefix = `--${responseBatchBoundary}${HTTP_LINE_ENDING}`;
const batchResponseEnding = `--${responseBatchBoundary}--`

let requestBatchBoundary = undefined;

// Parse content type for sub request boundary
const contentType = context.request!.getHeader("content-type");
if (contentType === undefined || contentType === '') {
error = new StorageError(
400,
"MissingRequiredHeader",
"An HTTP header that's mandatory for this request is not specified.",
context.contextId!,
{
HeaderName: "Content-Type"
}
);
}
else {
const contentTypeValues = contentType!.split(";");

contentTypeValues.forEach(contentTypeValue => {
contentTypeValue = contentTypeValue.trim();
if (contentTypeValue.startsWith("boundary=")) {
requestBatchBoundary = contentTypeValue.substring(9);
}
});
}

if (requestBatchBoundary === undefined) {
error = new StorageError(
400,
"InvalidHeaderValue",
"The value for one of the HTTP headers is not in the correct format.",
context.contextId!,
perRequestPrefix,
batchRequestEnding,
subRequestPathPrefix,
batchRequest,
requestBody);
} catch (err) {
if ((err instanceof MiddlewareError)
&& err.hasOwnProperty("storageErrorCode")
&& err.hasOwnProperty("storageErrorMessage")
&& err.hasOwnProperty("storageRequestID")) {
error = err;
{
HeaderName: "Content-Type",
HeaderValue: contentType!
}
);
}
else {
const requestBody = await this.requestBodyToString(body);
const perRequestPrefix = `--${requestBatchBoundary}${HTTP_LINE_ENDING}`;
const batchRequestEnding = `--${requestBatchBoundary}--`
try {
subRequests = await this.parseSubRequests(
context.contextId!,
perRequestPrefix,
batchRequestEnding,
subRequestPathPrefix,
batchRequest,
requestBody);
} catch (err) {
if ((err instanceof MiddlewareError)
&& err.hasOwnProperty("storageErrorCode")
&& err.hasOwnProperty("storageErrorMessage")
&& err.hasOwnProperty("storageRequestID")) {
error = err;
}
else {
error = new StorageError(
400,
"InvalidInput",
"One of the request inputs is not valid.",
context.contextId!
);
}
}
else {

if (subRequests && subRequests.length > 256) {
error = new StorageError(
400,
"InvalidInput",
"One of the request inputs is not valid.",
"ExceedsMaxBatchRequestCount",
"The batch operation exceeds maximum number of allowed subrequests.",
context.contextId!
);
}
}

const subResponses: BlobBatchSubResponse[] = [];
if (subRequests && subRequests.length > 256) {
error = new StorageError(
400,
"ExceedsMaxBatchRequestCount",
"The batch operation exceeds maximum number of allowed subrequests.",
context.contextId!
);
}

if (error) {
this.logger.error(
`BlobBatchHandler: ${error.message}`,
Expand All @@ -523,7 +572,10 @@ export class BlobBatchHandler {
}
}

return this.serializeSubResponse(perRequestPrefix, batchRequestEnding, subResponses);
return {
contentType: "multipart/mixed; boundary=" + responseBatchBoundary,
reponseBody: this.serializeSubResponse(perResponsePrefix, batchResponseEnding, subResponses)
};
}

private HandleOneSubRequest(request: IRequest,
Expand Down
9 changes: 3 additions & 6 deletions src/blob/handlers/ContainerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,19 +338,16 @@ export default class ContainerHandler extends BaseHandler
options: Models.ContainerSubmitBatchOptionalParams,
context: Context): Promise<Models.ContainerSubmitBatchResponse> {
const blobServiceCtx = new BlobStorageContext(context);
const requestBatchBoundary = blobServiceCtx.request!.getHeader("content-type")!.split("=")[1];

const blobBatchHandler = new BlobBatchHandler(this.accountDataStore, this.oauth,
this.metadataStore, this.extentStore, this.logger, this.loose, this.disableProductStyle);

const responseBodyString = await blobBatchHandler.submitBatch(body,
requestBatchBoundary,
const batchResponse = await blobBatchHandler.submitBatch(body,
blobServiceCtx.request!.getPath(),
context.request!,
context);

const responseBody = new Readable();
responseBody.push(responseBodyString);
responseBody.push(batchResponse.reponseBody);
responseBody.push(null);

// No client request id defined in batch response, should refine swagger and regenerate from it.
Expand All @@ -359,7 +356,7 @@ export default class ContainerHandler extends BaseHandler
statusCode: 202,
requestId: context.contextId,
version: BLOB_API_VERSION,
contentType: "multipart/mixed; boundary=" + requestBatchBoundary,
contentType: batchResponse.contentType,
body: responseBody
};

Expand Down
10 changes: 3 additions & 7 deletions src/blob/handlers/ServiceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,20 +125,16 @@ export default class ServiceHandler extends BaseHandler
options: Models.ServiceSubmitBatchOptionalParams,
context: Context
): Promise<Models.ServiceSubmitBatchResponse> {
const blobServiceCtx = new BlobStorageContext(context);
const requestBatchBoundary = blobServiceCtx.request!.getHeader("content-type")!.split("=")[1];

const blobBatchHandler = new BlobBatchHandler(this.accountDataStore, this.oauth,
this.metadataStore, this.extentStore, this.logger, this.loose, this.disableProductStyle);

const responseBodyString = await blobBatchHandler.submitBatch(body,
requestBatchBoundary,
const batchResponse = await blobBatchHandler.submitBatch(body,
"",
context.request!,
context);

const responseBody = new Readable();
responseBody.push(responseBodyString);
responseBody.push(batchResponse.reponseBody);
responseBody.push(null);

// No client request id defined in batch response, should refine swagger and regenerate from it.
Expand All @@ -147,7 +143,7 @@ export default class ServiceHandler extends BaseHandler
statusCode: 202,
requestId: context.contextId,
version: BLOB_API_VERSION,
contentType: "multipart/mixed; boundary=" + requestBatchBoundary,
contentType: batchResponse.contentType,
body: responseBody
};

Expand Down