From 1181951122bb80bfd27752a3d78b7c626145c663 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:38:04 +0000 Subject: [PATCH 1/5] Initial plan From fdd4919f879f2a26afff6571f98c6fa383180ac8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:40:50 +0000 Subject: [PATCH 2/5] Fix generated serializer UTF-8 chunk decoding Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- ChangeLog.md | 1 + src/blob/generated/utils/serializer.ts | 5 +- src/queue/generated/utils/serializer.ts | 5 +- src/table/generated/utils/serializer.ts | 5 +- tests/common/generatedSerializer.test.ts | 135 +++++++++++++++++++++++ 5 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 tests/common/generatedSerializer.test.ts diff --git a/ChangeLog.md b/ChangeLog.md index 71f381dd4..63f16d891 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,6 +10,7 @@ General: - Fixed `--oauth` startup validation and reporting. Azurite now reports supported OAuth levels consistently across command-line entrypoints and no longer crashes when `--oauth` is specified without a value. (issue #2525) - Fixed SharedKey and SharedKeyLite authentication when both `date` and `x-ms-date` headers are present, matching Azure Storage signing behaviour for Blob, Queue, and Table services. (issue #1385) - Removed npm and its transitive dependencies from the Linux Docker runtime image while retaining Node.js and existing JavaScript entrypoints, reducing the container attack surface and addressing npm-related vulnerability reports. (issue #2758) +- Fixed Blob, Queue, and Table request body decoding so multi-byte UTF-8 characters split across incoming chunks are preserved. (issue #2670) Blob: diff --git a/src/blob/generated/utils/serializer.ts b/src/blob/generated/utils/serializer.ts index 8689cbff4..529e2e1a9 100644 --- a/src/blob/generated/utils/serializer.ts +++ b/src/blob/generated/utils/serializer.ts @@ -182,15 +182,14 @@ export async function deserialize( async function readRequestIntoText(req: IRequest): Promise { return new Promise((resolve, reject) => { - const segments: string[] = []; + const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { segments.push(buffer); }); bodyStream.on("error", reject); bodyStream.on("end", () => { - const joined = segments.join(""); - resolve(joined); + resolve(Buffer.concat(segments).toString("utf8")); }); }); } diff --git a/src/queue/generated/utils/serializer.ts b/src/queue/generated/utils/serializer.ts index 267659fa5..3a69927ec 100644 --- a/src/queue/generated/utils/serializer.ts +++ b/src/queue/generated/utils/serializer.ts @@ -182,15 +182,14 @@ export async function deserialize( async function readRequestIntoText(req: IRequest): Promise { return new Promise((resolve, reject) => { - const segments: string[] = []; + const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { segments.push(buffer); }); bodyStream.on("error", reject); bodyStream.on("end", () => { - const joined = segments.join(""); - resolve(joined); + resolve(Buffer.concat(segments).toString("utf8")); }); }); } diff --git a/src/table/generated/utils/serializer.ts b/src/table/generated/utils/serializer.ts index 4b68f5eaa..173d13031 100644 --- a/src/table/generated/utils/serializer.ts +++ b/src/table/generated/utils/serializer.ts @@ -182,15 +182,14 @@ export async function deserialize( async function readRequestIntoText(req: IRequest): Promise { return new Promise((resolve, reject) => { - const segments: string[] = []; + const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { segments.push(buffer); }); bodyStream.on("error", reject); bodyStream.on("end", () => { - const joined = segments.join(""); - resolve(joined); + resolve(Buffer.concat(segments).toString("utf8")); }); }); } diff --git a/tests/common/generatedSerializer.test.ts b/tests/common/generatedSerializer.test.ts new file mode 100644 index 000000000..d25826dbd --- /dev/null +++ b/tests/common/generatedSerializer.test.ts @@ -0,0 +1,135 @@ +import { strict as assert } from "assert"; +import { Readable } from "stream"; + +import * as msRest from "@azure/ms-rest-js"; + +import BlobContext from "../../src/blob/generated/Context"; +import BlobIRequest from "../../src/blob/generated/IRequest"; +import BlobILogger from "../../src/blob/generated/utils/ILogger"; +import { deserialize as deserializeBlob } from "../../src/blob/generated/utils/serializer"; +import QueueContext from "../../src/queue/generated/Context"; +import QueueIRequest from "../../src/queue/generated/IRequest"; +import QueueILogger from "../../src/queue/generated/utils/ILogger"; +import { deserialize as deserializeQueue } from "../../src/queue/generated/utils/serializer"; +import TableContext from "../../src/table/generated/Context"; +import TableIRequest from "../../src/table/generated/IRequest"; +import TableILogger from "../../src/table/generated/utils/ILogger"; +import { deserialize as deserializeTable } from "../../src/table/generated/utils/serializer"; + +const requestBodyMapper: msRest.Mapper = { + serializedName: "body", + type: { + name: "String" + } +}; + +const spec = { + httpMethod: "POST", + path: "/", + urlParameters: [], + requestBody: { + parameterPath: "bodyParameter", + mapper: requestBodyMapper + }, + responses: {}, + serializer: new msRest.Serializer() +} as msRest.OperationSpec; + +const logger: BlobILogger & QueueILogger & TableILogger = { + error: () => undefined, + warn: () => undefined, + info: () => undefined, + verbose: () => undefined, + debug: () => undefined +}; + +type DeserializeCase = { + name: string; + deserialize: ( + context: any, + req: BlobIRequest & QueueIRequest & TableIRequest, + operationSpec: msRest.OperationSpec, + operationLogger: BlobILogger & QueueILogger & TableILogger + ) => Promise; + createContext: () => BlobContext | QueueContext | TableContext; +}; + +describe("Generated serializer readRequestIntoText @loki", () => { + const cases: DeserializeCase[] = [ + { + name: "Blob", + deserialize: deserializeBlob, + createContext: () => + new BlobContext({ generated: { contextID: "blob" } }, "generated") + }, + { + name: "Queue", + deserialize: deserializeQueue, + createContext: () => + new QueueContext({ generated: { contextID: "queue" } }, "generated") + }, + { + name: "Table", + deserialize: deserializeTable, + createContext: () => + new TableContext({ generated: { contextID: "table" } }, "generated") + } + ]; + + for (const testCase of cases) { + it(`${testCase.name} preserves multi-byte UTF-8 characters split across chunks`, async () => { + const expectedBodyParameter = "before 🌊 after"; + const request = createRequestWithSplitMultiByteCharacter( + JSON.stringify(expectedBodyParameter), + "🌊" + ); + + const parameters = (await testCase.deserialize( + testCase.createContext(), + request, + spec, + logger + )) as { bodyParameter: string; body: string }; + + assert.equal(parameters.bodyParameter, expectedBodyParameter); + assert.equal(parameters.body, JSON.stringify(expectedBodyParameter)); + }); + } +}); + +function createRequestWithSplitMultiByteCharacter( + body: string, + splitCharacter: string +): BlobIRequest & QueueIRequest & TableIRequest { + const bodyBuffer = Buffer.from(body, "utf8"); + const splitCharacterBuffer = Buffer.from(splitCharacter, "utf8"); + const splitCharacterIndex = bodyBuffer.indexOf(splitCharacterBuffer); + assert.notEqual(splitCharacterIndex, -1); + const splitIndex = splitCharacterIndex + 1; + let storedBody: string | undefined; + + const request = { + getMethod: () => "POST", + getUrl: () => "/", + getEndpoint: () => "/", + getPath: () => "/", + getBodyStream: () => + Readable.from([ + bodyBuffer.subarray(0, splitIndex), + bodyBuffer.subarray(splitIndex) + ]), + setBody: (newBody: string | undefined) => { + storedBody = newBody; + return request; + }, + getBody: () => storedBody, + getHeader: (field: string) => + field.toLowerCase() === "content-type" ? "application/json" : undefined, + getHeaders: () => ({ "content-type": "application/json" }), + getRawHeaders: () => [], + getQuery: () => undefined, + getProtocol: () => "http" + } as BlobIRequest & QueueIRequest & TableIRequest; + + return request; +} From c2360aaea9803efc5a5fe20f52a34929c778ae1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:43:33 +0000 Subject: [PATCH 3/5] Harden serializer chunk buffering Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/generated/utils/serializer.ts | 2 +- src/queue/generated/utils/serializer.ts | 2 +- src/table/generated/utils/serializer.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blob/generated/utils/serializer.ts b/src/blob/generated/utils/serializer.ts index 529e2e1a9..00b007403 100644 --- a/src/blob/generated/utils/serializer.ts +++ b/src/blob/generated/utils/serializer.ts @@ -185,7 +185,7 @@ async function readRequestIntoText(req: IRequest): Promise { const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { - segments.push(buffer); + segments.push(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)); }); bodyStream.on("error", reject); bodyStream.on("end", () => { diff --git a/src/queue/generated/utils/serializer.ts b/src/queue/generated/utils/serializer.ts index 3a69927ec..304b6f916 100644 --- a/src/queue/generated/utils/serializer.ts +++ b/src/queue/generated/utils/serializer.ts @@ -185,7 +185,7 @@ async function readRequestIntoText(req: IRequest): Promise { const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { - segments.push(buffer); + segments.push(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)); }); bodyStream.on("error", reject); bodyStream.on("end", () => { diff --git a/src/table/generated/utils/serializer.ts b/src/table/generated/utils/serializer.ts index 173d13031..da8cce50b 100644 --- a/src/table/generated/utils/serializer.ts +++ b/src/table/generated/utils/serializer.ts @@ -185,7 +185,7 @@ async function readRequestIntoText(req: IRequest): Promise { const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { - segments.push(buffer); + segments.push(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)); }); bodyStream.on("error", reject); bodyStream.on("end", () => { From 075a09683080ad9a460341d651e2fb617c8ca89a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:44:57 +0000 Subject: [PATCH 4/5] Clarify UTF-8 split regression helper Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- tests/common/generatedSerializer.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/common/generatedSerializer.test.ts b/tests/common/generatedSerializer.test.ts index d25826dbd..13ebc9331 100644 --- a/tests/common/generatedSerializer.test.ts +++ b/tests/common/generatedSerializer.test.ts @@ -103,9 +103,11 @@ function createRequestWithSplitMultiByteCharacter( ): BlobIRequest & QueueIRequest & TableIRequest { const bodyBuffer = Buffer.from(body, "utf8"); const splitCharacterBuffer = Buffer.from(splitCharacter, "utf8"); + assert.ok(splitCharacterBuffer.length > 1); const splitCharacterIndex = bodyBuffer.indexOf(splitCharacterBuffer); assert.notEqual(splitCharacterIndex, -1); - const splitIndex = splitCharacterIndex + 1; + const splitIndex = + splitCharacterIndex + Math.floor(splitCharacterBuffer.length / 2); let storedBody: string | undefined; const request = { From 0958c0e02c8ad58524a4caf2a69feb16548cddb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:46:37 +0000 Subject: [PATCH 5/5] Clarify serializer chunk handling Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/generated/utils/serializer.ts | 6 +++++- src/queue/generated/utils/serializer.ts | 6 +++++- src/table/generated/utils/serializer.ts | 6 +++++- tests/common/generatedSerializer.test.ts | 11 +++++++++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/blob/generated/utils/serializer.ts b/src/blob/generated/utils/serializer.ts index 00b007403..d71985db6 100644 --- a/src/blob/generated/utils/serializer.ts +++ b/src/blob/generated/utils/serializer.ts @@ -185,7 +185,11 @@ async function readRequestIntoText(req: IRequest): Promise { const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { - segments.push(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)); + segments.push( + typeof buffer === "string" + ? Buffer.from(buffer, "utf8") + : Buffer.from(buffer) + ); }); bodyStream.on("error", reject); bodyStream.on("end", () => { diff --git a/src/queue/generated/utils/serializer.ts b/src/queue/generated/utils/serializer.ts index 304b6f916..df89baea0 100644 --- a/src/queue/generated/utils/serializer.ts +++ b/src/queue/generated/utils/serializer.ts @@ -185,7 +185,11 @@ async function readRequestIntoText(req: IRequest): Promise { const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { - segments.push(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)); + segments.push( + typeof buffer === "string" + ? Buffer.from(buffer, "utf8") + : Buffer.from(buffer) + ); }); bodyStream.on("error", reject); bodyStream.on("end", () => { diff --git a/src/table/generated/utils/serializer.ts b/src/table/generated/utils/serializer.ts index da8cce50b..969a7ffc6 100644 --- a/src/table/generated/utils/serializer.ts +++ b/src/table/generated/utils/serializer.ts @@ -185,7 +185,11 @@ async function readRequestIntoText(req: IRequest): Promise { const segments: Buffer[] = []; const bodyStream = req.getBodyStream(); bodyStream.on("data", buffer => { - segments.push(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)); + segments.push( + typeof buffer === "string" + ? Buffer.from(buffer, "utf8") + : Buffer.from(buffer) + ); }); bodyStream.on("error", reject); bodyStream.on("end", () => { diff --git a/tests/common/generatedSerializer.test.ts b/tests/common/generatedSerializer.test.ts index 13ebc9331..0528bc023 100644 --- a/tests/common/generatedSerializer.test.ts +++ b/tests/common/generatedSerializer.test.ts @@ -103,9 +103,16 @@ function createRequestWithSplitMultiByteCharacter( ): BlobIRequest & QueueIRequest & TableIRequest { const bodyBuffer = Buffer.from(body, "utf8"); const splitCharacterBuffer = Buffer.from(splitCharacter, "utf8"); - assert.ok(splitCharacterBuffer.length > 1); + assert.ok( + splitCharacterBuffer.length > 1, + "splitCharacter must be a multi-byte character" + ); const splitCharacterIndex = bodyBuffer.indexOf(splitCharacterBuffer); - assert.notEqual(splitCharacterIndex, -1); + assert.notEqual( + splitCharacterIndex, + -1, + "splitCharacter must appear in body" + ); const splitIndex = splitCharacterIndex + Math.floor(splitCharacterBuffer.length / 2); let storedBody: string | undefined;