1

I wrote a function to catch non-200 results with fetch:

 1 function $get(url, callback) {
 2  fetch(url, {credentials: "same-origin"})
 3    .then(resp => {
 4      if (!resp.ok) {
 5        resp.text().then((mesg) => {
 6          throw {"stat": resp.status, "mesg": mesg.trim()}
 7        })
 8        return resp.text()
 9      } 
10      return resp.json() 
11    })
12    .then(data => callback({"stat": 200, "data": data}))
13    .catch(error => callback(error))
14}

I got error on line 9:

ERROR: TypeError: Failed to execute 'text' on 'Response': body stream already read

The reason that I have to write code shown in line 5~7 is that if I wrote:

if (!resp.ok) {
  throw {"stat": resp.status, "mesg": resp.statusText}
return resp.json()

I will get error message like {"stat": 403, "mesg": "Forbidden"}, while what I want is: {"stat": 403, "mesg": "invalid user name or password"}.

On the server side my go program will generate non-200 reply like this:

> GET /api/login?u=asdf&p=asdf HTTP/1.1
> Host: localhost:7887
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Sat, 17 Jul 2021 11:53:16 GMT
< Content-Length: 25
< 
invalid username or password

I.e. the go library do not modify http status text, instead, put error message in body, which maybe mandated by the http standard (e.g. status text cannot be changed).

So, my question is, either:

  • How to read the body of non-200 reply without using promise?
  • or, how to reply an "empty" promise in case of error, to prevent the stream being read again?

=== EDIT ===

The following code works OK, however, it seems to use "anti" pattern as pointed out by comments:

function $get(url, callback) {
  fetch(url, {credentials: "same-origin"})
    .then(resp => {
      if (!resp.ok) {
        resp.text().then((mesg) => {
          callback({"stat": resp.status, "mesg": mesg.trim()})
        })
        return new Promise(function(_, _) {}) 
      } 
      return resp.json()
    })
    .then(data => callback({"stat": 200, "data": data}))
    .catch(error => { console.log(`GET ${url}\nERROR: ${error}`) })
}

However, this doe not work:

function $get(url, callback) {
  fetch(url, {credentials: "same-origin"})
    .then(resp => {
      if (!resp.ok) {
        resp.text().then((mesg) => {
          throw `{"stat": resp.status, "mesg": mesg.trim()}`
        }) 
      } 
      return resp.json()
    })
    .then(data => callback({"stat": 200, "data": data}))
    .catch(error => { console.log(`GET ${url}\nERROR: ${error}`) })
}

The throw will generate this error, instead of passing control to the catch below:

127.0.0.1/:1 Uncaught (in promise) {"stat": resp.status, "mesg": mesg.trim()}
9
  • 4
    Don't mix promises with a callback system. It is an antipattern. Commented Jul 17, 2021 at 12:39
  • @trincot, so, my purpose is to let my code know what is the http status code as well as get its reponse body, without callbacks what is the "pro" pattern way? Commented Jul 17, 2021 at 12:51
  • The caller should use the promise object that your function should return... so you need to do return fetch(....... Then that returned promise has a then method with a callback that can be passed to it... or use await syntax. Commented Jul 17, 2021 at 13:04
  • Re your edit: "The following code works OK" - no it doesn't, it sometimes calls callback twice. "However, this doe not work:" - yes, you're missing the return keyword in front of the resp.text().then(…) promise chain. Re-read Poul's answer please. Commented Jul 17, 2021 at 13:07
  • @Bergi: I am a bit confused, and yes I tried Poul's code, and got error last time, and I tried again, it worked.... Commented Jul 17, 2021 at 13:14

2 Answers 2

3

Considering you are using fetch you can also use async/await and do the following. :

async function $get(url, callback) {
  try {
    const resp = await fetch(url, {credentials: "same-origin"});
    
    if (!resp.ok) {
      // this will end up in the catch statement below
      throw({ stat: resp.status, mesg: (await resp.text()).trim());
    }
    
    callback({ stat: 200, data: await resp.json() });
  } catch(error) {
    callback(error);
  }
}

I don't understand why you would use callback functions though :) those are so 1999


To explain your error, you are calling resp.text() twice when there is an error. To prevent that, you should immediately return the promise chained from the first resp.text() call. This will also throw the error and end up in the catch block without reaching the consecutive then() statement:

function $get(url, callback) {
 fetch(url, {credentials: "same-origin"})
   .then(resp => {
     if (!resp.ok) {
       return resp.text().then((mesg) => {
//     ^^^^^^
         throw {stat: resp.status, error: mesg.trim()}
       });
     } 
     return resp.json() ;
   })
   .then(data => callback({stat: 200, data }))
   .catch(error => callback(error))
}

A "proper" $get function which doesn't use callbacks:

function $get(url) {
  return fetch(url, {credentials: "same-origin"}).then(async (resp) => {
    const stat = resp.status;

    if (!resp.ok) {
      throw({ stat, error: (await resp.text()).trim() });
    }

    return { stat, data: await resp.json() };
  });
}

Which you can consume like:

$get('https://example.com')
  .then(({ stat, data }) => {

  })
  .catch({ stat, error}) => {

  })
Sign up to request clarification or add additional context in comments.

13 Comments

The first part of your answer maybe a good idea, but I haven't try it yet, because I found that by simply doing return new Promise(function(_, _) {}) will eliminate the error. The second part is wrong, I tried in chrome and god not found: undefined when handling wrong password
@xrfang That is an anti-pattern. You just need to return the first resp.text() promise as shown in second part of this answer. Then when you throw the error is gets passed down the chain
@xrfang don't create a new promise when you are already working with promises. That will clutter your code even more and will result in a then chain of hell. Same with the callback methods. To keep the code clean you are way better off using async/await,
@PoulKruijt sorry, your second part is not a solution just explain why I am wrong. Before trying the async/wait stuff, I have to explain that if I simply return resp.text() then in the callback, I will not be able to tell if there is an error or not, because I lost the http status code.
@xrfang Using async/await is not required. In your code just change the return and get rid of the second resp.text(). You will just return the first one that throws the error in then() instead and be able to catch() it further down the chain
|
1

Just return the rejected promise and you are good to go

 1 function $get(url, callback) {
 2  fetch(url, {credentials: "same-origin"})
 3    .then(resp => {
 4      if (!resp.ok) {
 5        return resp.text().then((mesg) => {
 6          throw {"stat": resp.status, "mesg": mesg.trim()}
 7        })
 9      } 
10      return resp.json() 
11    })
12    .then(data => callback({"stat": 200, "data": data}))
13    .catch(error => callback(error))
14}

1 Comment

@Bergi yeah, that was a legit point; and fixed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.