Retrieve data from a ReadableStream object?
Asked Answered
B

14

391

How may I get information from a ReadableStream object?

I am using the Fetch API and I don't see this to be clear from the documentation.

The body is being returned as a ReadableStream and I would simply like to access a property within this stream. Under Response in the browser dev tools, I appear to have this information organised into properties, in the form of a JavaScript object.

fetch('http://192.168.5.6:2000/api/car', obj)
    .then((res) => {
        if(!res.ok) {
            console.log("Failure:" + res.statusText);
            throw new Error('HTTP ' + res.status);
        } else {
            console.log("Success :" + res.statusText);
            return res.body // what gives?
        }
    })
Benedicite answered 2/11, 2016 at 16:35 Comment(5)
@FrancescoPezzella Thanks for the response. I have tried 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.Benedicite
So your issue is purely related to the error 400 condition? What happens if you change the handler to console.log(res.json());? Do you see the data you are expecting?Deign
@Benedicite Are you trying to read the response as a stream if res.status == 200?Thermosiphon
Is it just me or that documentation is plain wrong? I did fix it with the solutions on this answers though.Kathyrnkati
I know it has been a while but for the sake of keeping stackoverflow great, please just accept the right answer. The one with over 200 upvotes.Flaxman
S
500

In order to access the data from a ReadableStream you need to call one of the conversion methods (docs available here).

As an example:

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(function(response) {
    // The response is a Response instance.
    // You parse the data into a useable format using `.json()`
    return response.json();
  }).then(function(data) {
    // `data` is the parsed version of the JSON returned from the above endpoint.
    console.log(data);  // { "userId": 1, "id": 1, "title": "...", "body": "..." }
  });

EDIT: If your data return type is not JSON or you don't want JSON then use text()

As an example:

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(function(response) {
    return response.text();
  }).then(function(data) {
    console.log(data); // this will be a string
  });
Sedgemoor answered 3/11, 2016 at 13:51 Comment(9)
Thanks for the response. I have tried this and am still getting the same error where res.body is undefined. I am able to retrieve the status however in first then() function with res.status. It seems that only the body is a ReadableStream object. It does seem to have a property locked, which is set to true?Benedicite
Where are you trying to access res.body (this isn't part of my example)? Can you share some sample code in your original question to make it clearer where your problem might be.Deign
I tried accessing res.body from the json response that was returned in first .then() function. I have added a sample to my original question for more clarity. Thanks!Benedicite
Awesome, using react and request native, and wondering what in the world to do with a ReadableStream, and this did the trick. ++Zehe
Just a headsup, seems like a no-brainer, but make sure the backend you're hitting is actually providing valid JSON! Definitely not speaking from experience.Recalescence
What if the response of the ReadableStream is an image?Eclair
@Eclair It depends on what you want to do with the Image. You could call one of these methods developer.mozilla.org/en-US/docs/Web/API/Body, for example const imageBlob = await response.blob(); and then const objectUrl = URL.createObjectURL(imageBlob); htmlImageElement.src = objectUrl;. The returned URL is released automatically when the document is unloaded. But if your page has dynamic use, you should release it explicitly by calling window.URL.revokeObjectURL(objectUrl). Or use a FileReader.Fructidor
thank you so so so much for this resposne! I was bashing my face against everything not understanding all this "readable stream" api documentation and its all about chunks & text files and crap im not using. this worked! I haven't done backend work in over a year so i'm so rusty, this looks like what I used to learn/write and I knew I was forgetting something like this - this was it!Rubinstein
Link is broken, try this: developer.mozilla.org/en-US/docs/Web/API/ReadableStreamChargeable
F
123

Some people may find an async example useful:

var response = await fetch("https://httpbin.org/ip");
var body = await response.json(); // .json() is asynchronous and therefore must be awaited

json() converts the response's body from a ReadableStream to a json object.

The await statements must be wrapped in an async function, however you can run await statements directly in the console of Chrome (as of version 62).

Fagin answered 24/10, 2017 at 13:37 Comment(2)
Sometimes the real answer for you really is #2 haha, it makes sense why they'd make .json() asynchronous but it wasn't immediately obviousNeurologist
This is correct, and the most straightforward.Ninny
L
51

response.json() returns a Promise. Try ...

res.json().then(body => console.log(body));

where response is the result of the fetch(...)

Leonoraleonore answered 27/11, 2016 at 18:57 Comment(2)
I tried this, and it printed out the Promise instead of the body.Confidant
Try to chain the .then calls: fetch(...).then(res => res.json()).then(data => console.log(data))Humane
P
37

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 Uint8Arrays. 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 Uint8Arrays 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.

Parapet answered 22/6, 2022 at 16:6 Comment(2)
thanks for actually answering the questionLianna
For those who have a ReadableStream and just want a simple/easy one-liner to get the text out of it, a short hack is to wrap it in a new Response (or Request) object and then use the text method: await new Response(request.body).text(). Posted an answer for that here.Brushoff
B
29

For those who have a ReadableStream and want to get the text out of it, a short hack is to wrap it in a new Response (or Request) and then use the text method:

let text = await new Response(yourReadableStream).text();
Brushoff answered 28/10, 2022 at 15:0 Comment(0)
C
20

Little bit late to the party but had some problems with getting something useful out from a ReadableStream produced from a Odata $batch request using the Sharepoint Framework.

Had similar issues as OP, but the solution in my case was to use a different conversion method than .json(). In my case .text() worked like a charm. Some fiddling was however necessary to get some useful JSON from the textfile.

Cleavland answered 9/4, 2018 at 20:57 Comment(2)
Thank you! This worked for me. I am sending an Illuminate http response from my Laravel server with a simple return $data;. I was finally able to read this response in the browser with fetch(...).then(response => response.text()).then(data => console.log(data));Dumbstruck
i have an api which return jwt token and .then(response => response.text()).then(data => console.log(data)); return undefined in that caseUnpremeditated
B
20

Note that you can only read a stream once, so in some cases, you may need to clone the response in order to repeatedly read it:

fetch('example.json')
  .then(res=>res.clone().json())
  .then( json => console.log(json))

fetch('url_that_returns_text')
  .then(res=>res.clone().text())
  .then( text => console.log(text))
Botvinnik answered 8/5, 2020 at 0:19 Comment(0)
U
15

If you just want the response as text and don't want to convert it into JSON, use https://developer.mozilla.org/en-US/docs/Web/API/Body/text and then then it to get the actual result of the promise:

fetch('city-market.md')
  .then(function(response) {
    response.text().then((s) => console.log(s));
  });

or

fetch('city-market.md')
  .then(function(response) {
    return response.text();
  })
  .then(function(myText) {
    console.log(myText);
  });
Unify answered 2/7, 2018 at 11:30 Comment(1)
Yay for CityMarket.md <3Geiss
A
4

I dislike the chaining thens. The second then does not have access to status. As stated before 'response.json()' returns a promise. Returning the then result of 'response.json()' in a acts similar to a second then. It has the added bonus of being in scope of the response.

return fetch(url, params).then(response => {
    return response.json().then(body => {
        if (response.status === 200) {
            return body
        } else {
            throw body
        }
    })
})
Annabell answered 10/11, 2018 at 3:11 Comment(2)
The chaining then helps you retrieve the final resolved value (the body). Nesting them prevents you from being able to get the body value without a callback or some mechanism of the sort. Imagine this: let body = await fetch(...).then(res => res.json()).then(data => data). This wouldn't work in the nested way. To check for response.status you can always throw an exception inside the first then, and add a catch to the whole promise chain.Humane
AGREE. Preferred in enterprise environment. : )Gabble
G
4

Another approach could be to consume/process the incoming stream of data in chunks:

async function toJSON(body) {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  const chunks = [];

  async function read() {
    const { done, value } = await reader.read();

    if (done) {
      return JSON.parse(chunks.join(''));
    }

    const chunk = decoder.decode(value, { stream: true });
    chunks.push(chunk);
    return read();
  }

  return read();
}

const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const jsonData = await toJSON(response.body);

console.log(jsonData);

This approach is especially useful when the incoming data is too large or if you wish to start processing the data as soon as its chunks are available.

Wrote a blog post for those interested in learning more.

Guipure answered 19/6, 2023 at 23:31 Comment(1)
Other answers lose the interest of using a readable stream by calling methods that wait and return the final result at the end whereas yours allows to use the incoming data. Keep up the good work. Your blog post is helpful to me.Pacifica
J
1

here is how I implemented it. In this case the api is returning a ndjson as a stream, and I am reading it in chunks. In ndjson format, data is split by new lines, so each line by itself is a basic json which I parsed and added to fetchedData variable.

var fetchedData = [];

fetch('LinkGoesHere', {
    method: 'get',
    headers: {
        'Authorization': 'Bearer TokenGoesHere' // this part is irrelevant and you may not need it for your application
    }
})
.then(response => {
    if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.body.getReader();
})
.then(reader => {
    let partialData = '';

    // Read and process the NDJSON response
    return reader.read().then(function processResult(result) {
        if (result.done) {
            return;
        }

        partialData += new TextDecoder().decode(result.value, { stream: true });
        const lines = partialData.split('\n');

        for (let i = 0; i < lines.length - 1; i++) {
            const json = JSON.parse(lines[i]);
            fetchedData.push(json); // Store the parsed JSON object in the array
        }

        partialData = lines[lines.length - 1];

        return reader.read().then(processResult);
    });
})
.then(() => {
    // At this point, fetchedData contains all the parsed JSON objects
    console.log(fetchedData);
})
.catch(error => {
    console.error('Fetch error:', error);
});
Jambalaya answered 15/8, 2023 at 14:38 Comment(0)
A
0

const resText = await response.json(); //returns string

const resJson = JSON.parse(resText); //objectified

sometimes (especially when you are using an api written in another language), response.json returns string. You should treat it like res.body and parse it, then you have the object. Hope it helps.

Anaclitic answered 4/9, 2023 at 9:33 Comment(1)
const resText = await response.json(); returns a JSON object not a string, the second line will throw an error, trying to parse a JSON.Nordin
B
0

If you are using React Native, it used to not be possible to do this.

But streaming is now possible with https://github.com/react-native-community/fetch.

This was actually a bug that was never addressed by RN team for a while, and this repo emerged to provide a better fetch that complies with WHATWG Spec

This is a fork of GitHub's fetch polyfill, the fetch implementation React Native currently provides. This project features an alternative fetch implementation directy built on top of React Native's Networking API instead of XMLHttpRequest for performance gains. At the same time, it aims to fill in some gaps of the WHATWG specification for fetch, namely the support for text streaming.

Here's how to use it:

Install

This concise steps are from hours of debugging, and I dont want to waste your time.

$ npm install react-native-fetch-api --save

Now install polyfills:

$ npm install react-native-polyfill-globals

Use the polyfill with fetch:

Add the following code to the top of your app's entry file, index.js, located at the root of your project. Now your new Fetch is available globally.

import { polyfill as polyfillFetch } from 'react-native-polyfill-globals/src/fetch';
polyfill();

Now you can use the stream object like the normal browser fetch. Make sure to specify the option textStreaming true.

fetch('https://jsonplaceholder.typicode.com/todos/1', { reactNative: { textStreaming: true } })
  .then(response => response.body)
  .then(stream => ...)

Hope this helps!

Bullhorn answered 12/9, 2023 at 12:30 Comment(0)
A
-3

I just had the same problem for over 12 hours before reading next, just in case this helps anyone. When using nextjs inside your _api page you will need to use JSON.stringify(whole-response) and then send it back to your page using res.send(JSON.stringify(whole-response)) and when it's received on the client side you need to translate it back into json format so that it's usable. This can be kinda figured out by reading their serialization section. Hope it helps.

Apartment answered 27/5, 2022 at 13:37 Comment(2)
Consider formatting your codeUnilateral
I had the same problem about response.json() is undefined and it looks like that i did not await the fefch method: Always make sure to await asynchronous actions guys , const response = await fetch(url+path)Troudeloup

© 2022 - 2024 — McMap. All rights reserved.