Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uint8Array data corruption in node:https.request #24571

Closed
fushihara opened this issue Jul 13, 2024 · 0 comments · Fixed by #24603
Closed

Uint8Array data corruption in node:https.request #24571

fushihara opened this issue Jul 13, 2024 · 0 comments · Fixed by #24603
Assignees
Labels
bug Something isn't working correctly node compat

Comments

@fushihara
Copy link

fushihara commented Jul 13, 2024

There is a complete reproduction code.
https://github.com/fushihara/deno-aws-sdk-issue

deno version is deno 1.45.1

When a Uint8Array created under certain conditions from the ReadStream of a file is passed to ClientRequest.end for http communication
The value received on the server side will be a different byte string.

This issue is derived from the following URL
#24551

The following is how the reproduced code is executed.
Normally, all hash values should be the same ( "749d27efcde2194c9b0d6692bad5880740f90e70a5e4200b3e0ace34744b3102 " ), but the hash values on the server side of the deno are different.
This problem does not occur with node.

Deno

> cd 1-deno
> deno --version
deno 1.45.1 (release, x86_64-pc-windows-msvc)
v8 12.7.224.12
typescript 5.5.2

> deno run -A index.ts
start program
runtime is "Deno"
hash 749d27efcde2194c9b0d6692bad5880740f90e70a5e4200b3e0ace34744b3102 vs 6fdb427a9655231378d35541c14ded99a0c7fc7ccfd8b8d33db4312bc73e6feb
all success

node.js

> cd 2-node
> node --version
v22.4.1

> npm i && npm run start
start program
runtime is "Node"
hash 749d27efcde2194c9b0d6692bad5880740f90e70a5e4200b3e0ace34744b3102 vs 749d27efcde2194c9b0d6692bad5880740f90e70a5e4200b3e0ace34744b3102
all success

The reproduced code is as follows The code is the same as the one in the repository.
Executing this reproduced code will result in communication to httpbin.org.

Uint8Array operations are extracted from the following library.
This code works fine in node.js, so I determined that it was a deno issue.
https://github.com/aws/aws-sdk-js-v3/blob/b963a513006b289a3db0b4e6406f92f5e09a29c2/lib/lib-storage/src/Upload.ts#L326

import { Readable } from "node:stream";
import { request } from "node:https";
import { Buffer } from "node:buffer"
import { createReadStream } from "node:fs";
console.log(`start program`);
(async () => {
  const runtime = (() => {
    //@ts-ignore
    if (globalThis.Deno) {
      return "Deno";
    } else {
      return "Node";
    }
  })();
  const body = createReadStream("./test.bin");
  const dataFeeder = getChunkStreamx(body, 5_242_880);
  type hash = {
    willSendBinaryHash: string;
    serverReceivedBinaryHash: string;
  };
  let v1: hash | null = null;
  let v2: hash | null = null;
  for await (const dataPart of dataFeeder) {
    if (dataPart.partNumber == 2) {
      v2 = await getBufferSha256Hash(dataPart.data);
    } else if (dataPart.partNumber == 1) {
      v1 = await getBufferSha256Hash(dataPart.data);
    }
  }
  if (v1 && v2) {
    console.log(`runtime is "${runtime}"`);
    const v1Hash = "e315c42e74dd287ec00f608965e6c1dad45c3d2560255148c1e985cd404b5bc0";
    const v2Hash = "749d27efcde2194c9b0d6692bad5880740f90e70a5e4200b3e0ace34744b3102";
    if (runtime == "Deno") {
      assert(v1.serverReceivedBinaryHash == v1Hash);
      assert(v1.willSendBinaryHash == v1Hash);
      assert(v2.serverReceivedBinaryHash == "6fdb427a9655231378d35541c14ded99a0c7fc7ccfd8b8d33db4312bc73e6feb");
      assert(v2.willSendBinaryHash == v2Hash);
    } else if (runtime == "Node") {
      assert(v1.serverReceivedBinaryHash == v1Hash);
      assert(v1.willSendBinaryHash == v1Hash);
      assert(v2.serverReceivedBinaryHash == v2Hash);
      assert(v2.willSendBinaryHash == v2Hash);
    }
    console.log(`hash ${v2.willSendBinaryHash} vs ${v2.serverReceivedBinaryHash}`);
  } else {
    throw new Error("value not set");
  }
  console.log(`all success`);
})();
function assert(flag: boolean) {
  if (flag == false) {
    throw new Error();
  }
}
async function* getChunkStreamx(data: Readable, partSize: any) {
  let partNumber = 1;
  let chunks: Uint8Array[] = [];
  let bufLength = 0;
  for await (const chunk of data) {
    if (!(chunk instanceof Uint8Array)) {
      continue;
    }
    chunks.push(chunk);
    bufLength += chunk.byteLength;
    while (bufLength > partSize) {
      const dataChunk = Buffer.concat(chunks);
      const newData: Uint8Array = dataChunk.subarray(0, partSize);
      yield {
        partNumber,
        data: newData,
      };
      chunks = [dataChunk.subarray(partSize)];
      bufLength = chunks[0].byteLength;
      partNumber += 1;
    }
  }
  if (chunks.length !== 1) {
    throw new Error();
  }
  const resultData1 = chunks[0];
  yield {
    partNumber,
    data: resultData1,
    lastPart: true
  };
}


async function getBufferSha256Hash(requestBody: Uint8Array) {
  async function buffer2Sha256Hex(input: Uint8Array) {
    const buf2hex = (arrayBuffer: ArrayBuffer) => {
      return [...new Uint8Array(arrayBuffer)]
        .map(x => x.toString(16).padStart(2, '0')).join('');
    }
    const hashBuffer = await crypto.subtle.digest("SHA-256", input);
    const calHex = buf2hex(hashBuffer);
    return calHex;
  }
  const serverReceivedBin = await getHttpRequestSendUint8Array(requestBody);
  const serverReceivedBinaryHash = await buffer2Sha256Hex(serverReceivedBin);
  const willSendBinaryHash = await buffer2Sha256Hex(requestBody);
  return {
    willSendBinaryHash,
    serverReceivedBinaryHash
  }
};

async function getHttpRequestSendUint8Array(buf: Uint8Array) {
  const receiveBuffer = await new Promise<Buffer>(resolve => {
    const reqHttpbin = request({
      host: "httpbin.org",
      method: "PUT",
      path: "/anything",
    }, (res) => {
      const datas: Buffer[] = [];
      res.on('data', (chunk) => {
        if (Buffer.isBuffer(chunk)) {
          datas.push(chunk);
        }
      }); 
      res.on('end', () => {
        const buffer = Buffer.concat(datas);
        resolve(buffer);
      });
    });
    reqHttpbin.end(buf);
  });
  function decodeBase64(str: string) {
    const buffer = Buffer.from(str, 'base64');
    return buffer;
  }
  const httpBinResponseObj = JSON.parse(new TextDecoder().decode(receiveBuffer));
  const serverReceivedBin = decodeBase64(String(httpBinResponseObj.data).replace("data:application/octet-stream;base64,", ""));
  return serverReceivedBin;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working correctly node compat
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants