diff --git a/.changeset/metal-emus-chew.md b/.changeset/metal-emus-chew.md new file mode 100644 index 00000000000..2de6ed60a9a --- /dev/null +++ b/.changeset/metal-emus-chew.md @@ -0,0 +1,5 @@ +--- +"@smithy/fetch-http-handler": patch +--- + +Add polyfill to collect Blob in react-native environments diff --git a/packages/fetch-http-handler/src/stream-collector.ts b/packages/fetch-http-handler/src/stream-collector.ts index 1062a9d3e00..071a70fc961 100644 --- a/packages/fetch-http-handler/src/stream-collector.ts +++ b/packages/fetch-http-handler/src/stream-collector.ts @@ -1,13 +1,23 @@ import { StreamCollector } from "@smithy/types"; +import { fromBase64 } from "@smithy/util-base64"; export const streamCollector: StreamCollector = async (stream: Blob | ReadableStream): Promise => { if ((typeof Blob === "function" && stream instanceof Blob) || stream.constructor?.name === "Blob") { - return new Uint8Array(await (stream as Blob).arrayBuffer()); + if (Blob.prototype.arrayBuffer !== undefined) { + return new Uint8Array(await (stream as Blob).arrayBuffer()); + } + return collectBlob(stream as Blob); } return collectStream(stream as ReadableStream); }; +async function collectBlob(blob: Blob): Promise { + const base64 = await readToBase64(blob); + const arrayBuffer = fromBase64(base64); + return new Uint8Array(arrayBuffer); +} + async function collectStream(stream: ReadableStream): Promise { const chunks = []; const reader = stream.getReader(); @@ -32,3 +42,26 @@ async function collectStream(stream: ReadableStream): Promise { return collected; } + +function readToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + // reference: https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL + // response from readAsDataURL is always prepended with "data:*/*;base64," + if (reader.readyState !== 2) { + return reject(new Error("Reader aborted too early")); + } + const result = (reader.result ?? "") as string; + // Response can include only 'data:' for empty blob, return empty string in this case. + // Otherwise, return the string after ',' + const commaIndex = result.indexOf(","); + const dataOffset = commaIndex > -1 ? commaIndex + 1 : result.length; + resolve(result.substring(dataOffset)); + }; + reader.onabort = () => reject(new Error("Read aborted")); + reader.onerror = () => reject(reader.error); + // reader.readAsArrayBuffer is not always available + reader.readAsDataURL(blob); + }); +}