You may have asked the wrong question to solve your problem, but here is an answer to your actual question. An inspiration may be the source code of the Node.js stream/consumers
module.
res.body
is a ReadableStream
that emits chunks as Uint8Array
s. Note that ReadableStream
objects created elsewhere may emit other data types than Uint8Array
, and the methods outlined in this answer need to be adjusted in those cases.
There are multiple ways to consume such a stream:
Convert a stream to a Uint8Array
Using new Response(stream).arrayBuffer()
If you want to retrieve the whole content of the stream in one go, the easiest way would is to wrap it in a Response
object. You can then use one of the several methods to retrieve the object as a string, a JSON object, an array buffer or something else. For example, to retrieve it as an array buffer:
export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
return new Uint8Array(await new Response(stream).arrayBuffer());
}
Note that new Response(stream)
only works for Uint8Array
streams. If the stream emits any other type (such as strings), it will result in a TypeError: Received non-Uint8Array chunk
error.
Using stream.getReader()
The following function will collect all the chunks in a single Uint8Array
:
function concatArrayBuffers(chunks: Uint8Array[]): Uint8Array) {
const result = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
const chunks: Uint8Array[] = [];
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
} else {
chunks.push(value);
}
}
return concatArrayBuffers(chunks);
}
Using async iterator
ReadableStream
implements the async iterator protocol. However, this is not supported by most browsers yet, but in Node.js you can already use it (using TypeScript, you will have to use the NodeJS.ReadableStream
interface, see this discussion).
The following code will collect all the chunks into a single Uint8Array
:
export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
const chunks: Uint8Array[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
return concatArrayBuffers(chunks);
}
In the future when browser support Array.fromAsync()
, this can be shortened to:
export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
return concatArrayBuffers(await Array.fromAsync(stream));
}
Convert a stream to a string
Using new Response(stream).text()
Just like described above for an array buffer, a stream of Uint8Array
can be converted to a string by using Response.text()
:
export async function streamToString(stream: ReadableStream<Uint8Array>): Promise<string> {
return await new Response(stream).text();
}
Using TextDecoderStream
TextDecoderStream
will convert the stream of UInt8Array
chunks into a stream of string
chunks. This way you can collect the contents of a stream as a string directly. Note that browser support in Firefox has only been added in September 2022, so you might not want to use this in production just yet.
export async function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
let result = '';
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
result += value;
}
return result;
}
In browsers that support it, you can also consume this stream of strings using the async iterator protocol:
export async function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
let result = '';
for (const chunk of stream.pipeThrough(new TextDecoderStream()).getReader())
result += chunk;
}
return result;
}
Or in browsers that support Array.fromAsync()
, even shorter:
export async function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
const chunks = await Array.fromAsync(stream.pipeThrough(new TextDecoderStream()).getReader()));
return chunks.join("");
}
Using TextDecoder
To convert the Uint8Array
s generated by some of the functions above to a string, you can then use TextDecoder
:
const buffer = await streamToArrayBuffer(res.body);
const text = new TextDecoder().decode(buffer);
Note that this should only been used on the whole content, not on individual UInt8Array
chunks, as some characters may consist of multiple bytes and might be split up between chunks.
Convert a stream to a JSON object
Using new Response(stream).json()
Just like described above for an array buffer and a string, a stream of Uint8Array
can be parsed as JSON by using Response.json()
:
export async function streamToJson(stream: ReadableStream<Uint8Array>): Promise<unknown> {
return await new Response(stream).json();
}
Using JSON.parse()
Use any of the methods above to convert the stream to a string and then use JSON.parse()
to parse that string.
response.Body.json()
, but I am getting italic TypeError: Cannot read property 'json' of undefined italic . Is this because the bodyUsed property is also set to false? However I can view this body under the response tab in browser developer tools. There is an error message which I'd like to retrieve. – Benediciteconsole.log(res.json());
? Do you see the data you are expecting? – Deignres.status == 200
? – Thermosiphon