async-library / react-async

🍾 Flexible promise-based React data loader

Home Page:https://docs.react-async.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

onResolve doesn't trigger if response is empty

jargot opened this issue · comments

Hello,

Thanks for the library :)

I'm looking at the useFetch hook and specifically the associated onResolve callback to make a call to my backend and redirect to another URL if the call is successful using this pattern:

let { ... } = useFetch(`${url}`, withAuth(meta, token), {
   defer: true,
   onResolve: data => {
     console.log("Just resolved", data);
     // Redirect will go here
   }
 });

I have absolutely no problem reaching my console.log if I return data in my response in my Express backend like so:

return res.json({
    status: "ok"
});

However, sending an empty 200 response like so:

return res.sendStatus(200);

never triggers the onResolve callback and the console.log is never printed.

Is this the intendend behavior ?

If it is, maybe editing the documentation and mentionning the required data in the response would be helpful.

I'm happy to have a look and PR if needed :)

Hi,

That's unexpected behavior. Can you try setting an onReject handler as well, and see if that gets invoked when you do res.sendStatus(200)?

I tried setting up an Express route like that myself:

app.use('/foo', (req, res) => {
  return res.sendStatus(200);
});

When requested from the browser with fetch it gives me:

status: 200
ok: true
statusText: "OK"

and OK as the response body. So the body is not empty, but it's not valid JSON. I suspect your withAuth is expecting to always receive JSON?

My withAuth function returns this:

{
  "method": "PUT",
  "headers": {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Authorization": "Bearer ey..."
  }
}

I tried using an the onReject callback as well and got

SyntaxError: Unexpected token O in JSON at position 0

in this line

Changing my Accept header to something like:Accept: "application/json, */*" makes the onResolve work as intended by the way, so I'm not sure what would be the best library behavior in this scenario, as accepting a wildcard seems like a bad idea, but maybe it's every application developer that should find the right Accept headers for the right scenario,

Maybe the best solution would simply be a better error on the onReject and/or a mention in the docs ?

As your withAuth returns the "Accept": "application/json" header, useFetch assumes that the response from the server is JSON and tries to parse it as JSON. But the server returns an empty response - that cannot be parsed as JSON by JavaScript.

Try running JSON.parse(""), you'll get SyntaxError: JSON.parse: unexpected end of data at line 1 column 1 of the JSON data as error (slightly different behaviour in other browsers).

Either your server needs to return valid JSON or your headers should not indicate that the server should return JSON.

I'd say all of this is exactly expected behaviour: the server is returning invalid JSON although you specificly requested JSON from the server.

@phryneas: This makes a lot of sense and I realise my mistake :)

What's still confusing, and why I think react-async might benefit from a small tweak somewhere, is that using a simple fetch itself does resolve even with the wrong headers like so:

  fetch(`${url}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      Authorization:
        "Bearer eyJ0e..."
    },
    body: JSON.stringify(payload)
  })
    .then(res => {
      console.log("will enter here even with the wrong Accept header");
    })
    .catch(e => {
      console.log("error", e);
    });

Yes, but fetchis a lowest-level implementation, useFetch is not.

Realistically, when using fetch and specifying that accept header value, you would almost always use this code:

  fetch(`${url}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      Authorization:
        "Bearer eyJ0e..."
    },
    body: JSON.stringify(payload)
  })
    .then(res => {
      console.log("will enter here even with the wrong Accept header");
+      return res.json();
+    })
+    .then(json => {
+      console.log("will never get here if it is not valid JSON");
    })
    .catch(e => {
      console.log("error", e);
    });

Because, if you never cared about the JSON return value in the first place, there's just no reason to specify the accept header at all. Just let the server return what it wants, obviously you're not goint to parse the response.

Of course, useFetch could make it mandatory to call it with useFetch(url, { header: { accept: 'application/json' } }, { json: true }), but that's just noise for 99% of the userbase.

So, useFetch makes the following assumption: "if accept is set to application/json and json is not set, json defaults to true". This still means that if you want to send that header and do not want the response to be parsed, you can specify { json: false } in the options parameter. But that's just an escape hatch as that behaviour would be counterintuitive for most users.

We should change the automatic json deserialization to look at Content-Type instead of Accept. The former indicates what the server response contains, the latter indicates what kind of request body it accepts. Looking at the Accept header is a mistake.

Additionally, I would like to extend the json deserialization to error responses as well. Many servers return a json-encoded error message in the body for error responses, which currently is hard to retrieve with useFetch.

We should change the automatic json deserialization to look at Content-Type instead of Accept. The former indicates what the server response contains, the latter indicates what kind of request body it accepts. Looking at the Accept header is a mistake.

Nope, it's right this way. Accept for the kind of response we want to see from the server, content-type for the type of data we're sending. From MDN:

The Accept request HTTP header advertises which content types, expressed as MIME types, the client is able to understand. Using content negotiation, the server then selects one of the proposals, uses it and informs the client of its choice with the Content-Type response header

The Content-Type entity header is used to indicate the media type of the resource. [...] In requests, (such as POST or PUT), the client tells the server what type of data is actually sent.

Of course, we could look at the content-type of the response, but that would make the server dictate behaviour of the client and I guess that would lead to all kinds of weird behaviour.

We could look at the content-type of an error and, if we're waiting for JSON anyways because of the accept header or json option, use that information to decide if the error response should be deserialized.

Could we make it so that if an empty response is returned, then null gets returned, instead of trying to parse the response as JSON? In our use case, we always expect for the response to be JSON, except for some cases where an empty response is returned.

For example, if a DELETE fails, then it might return JSON, but if it succeeds, it just returns an empty response.

I suppose we could look at the Content-Length response header before trying to deserialize any JSON. I'd be happy to accept a PR for that.

Ah, yes, that sounds like a good middle ground!

Actually... perhaps an even better solution would be to only parse content as JSON when the request's accept header includes application/json and the response header is set to application/json.

Any thoughts on my last comment? 🙂

Sorry this took so long. I'm okay with requiring the server's response Content-Type to be application/json. However, that would be a breaking change. Not every server out there will set the proper content type, so this will have to be in a major release, which probably isn't going to happen soon. That's why I prefer to start by looking at the Content-Length response header, to see if at least we're getting some data. That could go in a patch release.

Care to open a PR (or two, one for each of these solutions)?