How do I perform HTTP requests

In a functional language we discourage attaching functions to data values; the data is instead passed into a method. This style is applied to JavaScript interop as well, which is why we have two arguments for then_(function, promise). When compiled to JavaScript, the interop logic swaps it back to the JavaScript style promise.then(function). This way developing code all looks the same but still produces the correct runtime code.

If you look at the JavaScript output from this code:

Fetch.fetch("https://api.icndb.com")
->Js.Promise.then_(Fetch.Response.text, _)
->Js.Promise.then_(text => print_endline(text)->Js.Promise.resolve, _);

will compile to the equivalent of

fetch("https://api.icndb.com")
  .then((x) => x.text)
  .then((text) => Promise.resolve((console.log(text))

in JavaScript. It won’t be quite that clean because the output is still ES5, but the result is the same.

2 Likes

We highly recommend just using plain XMLHTTPRequest over bs-fetch. Example:

// shape of your response
@bs.scope("JSON") @bs.val
external parseResponse: response => {"message": array<string>} = "parse"

let request = makeXMLHttpRequest()
request->addEventListener("load", () => {
  let response = request->response->parseResponse
  Js.log(response["message"])
})
request->open_("GET", "https://dog.ceo/api/breeds/image/random/3");
request->send

Here’s a gist with the old Reason syntax too: https://gist.github.com/chenglou/251ba7e6aa7c86c6ffdb301f420e934d

Benefits:

  • No runtime
  • Proper cancellation and proper error handlings (see gist). Network programming is about error recovery, not about happy paths.
  • Doesn’t need an extra library
  • No need to drag in Promise
  • Supported everywhere
  • Recommended with ReasonReact (as opposed to anything Promise-based, which are heavily discouraged in ReasonReact and Reactjs), because of cancellation
6 Likes

I also wrote this library some time ago. (Shameless self plug)

They are not zero cost bindings and it is an extra library you need to pull into your project but maybe it fits your use case :blush:. Or you just want to grab the xmlhttprequest bindings out there.

Just wanted to throw that onto the table.

5 Likes

Thank you for all the answers and clarifications.

@chenglou, this seems to be a lot of boilerplate for each developer to add to the code base…

It would be very convenient to have official ReScript bindings + examples to the standard Node + Web interfaces, ideally shipped with bs-platform. But it’s my newcomer opinion.

I would be very interested to know why Promise is discouraged in ReasonReact and React.js.

Thank you again.

3 Likes

Got it.
Thank you for the explamnation.

@mickeyvip I inlined the externals so that you can copy paste the whole thing for the playground. And yes, the boilerplate can go into a file or upstreamed into stdlib. I might look into that soon.

Taking away the bindings, the actual amount of code for a request is the same amount of line either way Except the XMLHTTPRequest solution has the advantages above. Also, you gotta count the lines from the library too if we’re doing lines count comparison. Either are easy to fit into a library.

See some of the bullet points earlier for why Promise is discouraged.

6 Likes

I’m pretty sure he made the question after reading bullet point list and considered that further explanation is needed. Also I think it is worth explaining what “we” means in this context. Is it a consensus on reason community? Is it just the rescript maintainers ? Some other group of people?

2 Likes

Sorry if it’s an offtopic, but:

  1. Do you discourage bs-fetch, or Fetch in general? I mean, Fetch already has cancellation in some browsers, and it also has streaming. So: is the Fetch API totally wrong or just not ready?
  2. Is the Promise overhead really noticeable when it comes to network requests?
2 Likes

If you don’t need cancellable HTTP requests, or you don’t need IE11 (or are happy to polyfill it), fetch is fine. Promises are fine. I’m not using using React, though, so that does colour my opinion on this.

I guess this explains why Js.Promise was never updated to pipe-first style. Oh well, I switched to https://github.com/aantron/promise anyway.

3 Likes

Thank you, @spyder.
I’ll look into that library.

  1. We discourage the former, and most likely the latter until it matures.
  2. It’s about the paradigm. Promise doesn’t have cancellation and is an inextensible API. Lemme repeat that network programming is about properly handling different failure conditions (among which is proper cancellation and cleanup), not about happy path programming.

Promises are always wrong in React. See https://gist.github.com/chenglou/b6cf738a5d7adbde2ee008eb93117b49 and the first comment in the gist.

4 Likes

If you have a larger app and want to avoid boilerplate you can also look into a GraphQL client. All data-fetching ceremony is abstracted away and you just describe data requirements in a declarative way.

3 Likes

:bulb: Seems like HTTP requests over XMLHTTPRequest deserves it’s own section in the ReScript docs. Would be great to see more examples on how to do HTTP POST with body and http headers, as well as a graphql example.

13 Likes

I just wonder of how using this for multiple http request? what I got is when one request is began to load, the other is cancelled.

What I’m doing is create request on every react component’s useEffect. I guess by creating request on every component should fix, but it’s not.

React.useEffect0(() => {
let request = makeXMLHttpRequest()
request->addEventListener("load", () => {
  let response = request->response->parseResponse
  setHomepage(_ => {message: response["message"]})
})
request->addEventListener("error", () => {
  Js.log("Error logging here")
})
request->open_("GET", url.homepage)

request->send

None
})

Edited: Ok I figured out how, I just need to make external binding on every component too.

1 Like

make sure to return a cleanup callback for your useEffect, bc that’s the whole point of using XmlHttpRequest.

Like here

2 Likes

Have there been developments in upstreaming this to the stdlib? I can’t find anything about this in the rescript docs. I don’t mean to sound demanding btw, I’m just curious since this was mentioned earlier last year. It’s a bit tedious to use the snippet to avoid promises (labeled as the wrong approach) since there’s a lot of boilerplate to do a simple GET.

2 Likes

I see some people complaining about boilerplate here, so I thought I’d share my HTTP Client for general feedback (or as an example for anyone that’s interested). It has:

  • ability to do a get request (still need to add post etc)
  • ability to unauthenticate a user or refresh auth token
  • comes with a provider component and a hook
// HTTPClient.res

open Http

type successCB = (Js.Json.t, status)=> unit
type errorCB = unit => unit
type cleanup = unit => unit

type t = {
  get: (~relativeURL: string, ~successCB: successCB, ~errorCB: errorCB) => cleanup,
  // post: (~relativeURL: string, ~successCB: successCB, ~errorCB: errorCB, ~body: Js.Json.t) => unit,
}

let newAuthenicatedClient = (~baseURL, ~unauthenticate, ~getAuthToken) => {
  let getTokenCB = cb => {
    open Promise
    getAuthToken()
    ->Promise.then(token => {
      cb(token)
      resolve()
    })
    ->catch(_ => {
      unauthenticate()
      resolve()
    })
    ->ignore
  }

  let get = (~relativeURL, ~successCB, ~errorCB) => {
    let request = makeXMLHttpRequest()
    request->addEventListener("load", () => {
      let status = request->statusCode->codeToStatus
      let response = request->response->toJson

      switch status {
      | StatusUnauthorized => unauthenticate()
      | _ => successCB(response, status)
      }
    })

    request->Http.addEventListener("error", () => {
      errorCB()
    })

    request->Http.open_("GET", baseURL ++ relativeURL)

    getTokenCB(token => {
      request->Http.setHeader("Authorization", "Bearer " ++ token)
      request->Http.send
    })

    () => request->abort
 }

  {
    get: get,
  }
}

let context = React.createContext(
  newAuthenicatedClient(
    ~baseURL="",
    ~unauthenticate=() => (),
    ~getAuthToken=() => Promise.resolve(""),
  ),
)

module Provider = {
  let provider = React.Context.provider(context)

  @react.component
  let make = (~client: t, ~children) => {
    React.createElement(provider, {"value": client, "children": children})
  }
}

let use = () => React.useContext(context)

It’s a lot, but the usage is quite nice (and most people can probably cut out a lot of the auth logic).

Usage:

Wrap the client somewhere at the top of your component tree (I’m using Auth0 btw, just if anyone is wondering where my token logic is coming from):

let client = HttpClient.newAuthenicatedClient(
      ~baseURL="http://localhost:4000",
      ~unauthenticate=() => logout({returnTo: Window.Location.origin}),
      ~getAuthToken=() => {
        open Promise
        getIdTokenClaims()->Promise.then(tokenClaims => resolve(tokenClaims.__raw))
      },
    )

<HttpClient.Provider client> children </HttpClient.Provider>

Then you can easily access the client anywhere in your component tree with the Hook:

let client = HttpClient.use()

Then you can inject it into some data layer, or run it in useEffect to cleanup on component unmount. You can do a GET request as follows:

React.useEffect0(() => {
     let cleanup = client.get(
      ~relativeURL="/some/relative/path",
      ~successCB=(json, status) => {
        Js.log(json)
      },
      ~errorCB=() => Js.log("error"),
    )

    Some(() => cleanup())
} 

I’d love some feedback, specifically around the Promise logic as that’s also new to me. Hope this is useful :slight_smile:

Lastly, I do have some added utils in my Http bindings, I’ll just dump the whole module here

// Http.res
type request
type response
type status =
  | StatusOk //200
  | StatusCreated //201
  | StatusNoContent //204
  | StatusBadRequest //400
  | StatusUnauthorized //401 unauthenticated
  | StatusForbidden //403 unauthorized
  | StatusNotFound //404
  | StatusInternalServerError //500

let codeToStatus = statusInt =>
  switch statusInt {
  | 200 => StatusOk
  | 201 => StatusCreated
  | 204 => StatusNoContent
  | 400 => StatusBadRequest
  | 401 => StatusUnauthorized
  | 403 => StatusForbidden
  | 404 => StatusNotFound
  | _ => StatusInternalServerError
  }

@new external makeXMLHttpRequest: unit => request = "XMLHttpRequest"
@send external addEventListener: (request, string, unit => unit) => unit = "addEventListener"
@get external response: request => response = "response"
@get external statusCode: request => int = "status"
@send external open_: (request, string, string) => unit = "open"
@send external setHeader: (request, string, string) => unit = "setRequestHeader"
@send external send: request => unit = "send"
@send external abort: request => unit = "abort"

@scope("JSON") @val
external toJson: response => Js.Json.t = "parse"
2 Likes

That’s kinda harsh, we have a way to treat unhappy paths with Promise.catch (I’m talking about Promise api in general) as stated above, fetch today has an option of cancelation in the most modern browsers (AbortController & AbortSignal | Can I use... Support tables for HTML5, CSS3, etc). I don’t think that chunk of code being repeated everywhere (or every project having to write its own wrapper) is a good solution, from Rescript own’s site: “Ever wanted a language like JavaScript, but without the warts…”, I think this kinda goes against all Rescript’s nicely thought syntax.

Pardon me the extreme, but imagine if webdev community had thought that jquery had a terrible API and have decided go back to use DOM directly.

2 Likes