Skip to content

Instantly share code, notes, and snippets.

@mikeal
Last active April 21, 2023 17:13
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikeal/013dbb2ef3810cfe976cbe16ddb11c6f to your computer and use it in GitHub Desktop.
Save mikeal/013dbb2ef3810cfe976cbe16ddb11c6f to your computer and use it in GitHub Desktop.
HTTP Client Comparison
const r2 = require('r2')
let doJsonThing = async (path, propname) => {
let res = await r2(`http://api.com${path}`).json
return res[propname]
}
const request = require('request')
let doJsonThing = (path, propname, cb) => {
request(`http://api.com${path}`, {json: true}, (err, resp, body) => {
if (err) return cb(err)
if (resp.statusCode < 200 || resp.statusCode > 299) {
return cb(new Error(`Status not 200, ${resp.statusCode}`))
}
cb(null, body[propname])
}
}
@mikeal
Copy link
Author

mikeal commented Aug 31, 2017

Note: uses r2 library here https://github.com/mikeal/r2

This is a good example of how much unnecessary API you can shave off once you have language level constructs for async.

The real advantage is that several classes of errors can be created and handled implicitly by the caller like any other method call.

  • Connection errors will automatically throw and propagate to the caller.
  • Waiting for a specific json property makes signaling the body encoding w/ an option unnecessary.
  • Because we know that we're waiting for a JSON body the underlying library knows that non-2xx codes are errors. They will be thrown and propagated without the user needing to do this manually.

@bluepnume
Copy link

bluepnume commented Aug 31, 2017

There's not that much less boilerplate when you consider the extra work r2 is doing in comparison to request. No reason you couldn't write an implementation of request which sees something like { onlyJson: true } and calls the callback with an error if status code is non-2xx -- there's nothing specific to async/await that allows the api to do that.

const request = require('request')

let doJsonThing = (path, propname, cb) => {
  request(`http://api.com${path}`, {onlyJson: true}, (err, body) => (err
      ? cb(err) : cb(null, body[propname])
  )
}

Although using res.json to signal that the user only wants the json body, in the form of a promise, is a nice touch.

Anyway, much of the remaining boilerplate is cut out by using promises instead of callbacks:

const request = require('request')

let doJsonThing = (path, propname) => {
  return request(`http://api.com${path}`, {onlyJson: true})
    .then(body => body[propname]);
}

(btw async/await is awesome and I love it, just let's call a spade a spade)

@bahmutov
Copy link

Agree with @bluepnume and want to point out that when using promises, getting property from returned JSON object is extra thing better left to functional library like Ramda. Also make a module that only gets JSON - since it is such a common case

const requestJson = require('request-json')
const R = require('ramda')
requestJson('http...').then(R.prop('user'))

@mikeal
Copy link
Author

mikeal commented Aug 31, 2017

So, I've been writing that 2xx checking boilerplate for the better part of 7 years in different projects. If there was an easy way to add it request, I would have done so a long time ago.

Here's the problem, you have to signal to the API that you want JSON support (which adds accept headers). This also sets up the JSON decoding. However, a fair number of APIs return valid JSON bodies for their error conditions.

Because there's just a single handler (the callback) for socket errors, response errors, and success we can't add default status code checking for this case in request itself without blocking people from being able to handle their own http errors codes that have json bodies. It has been considered several times.

The reason you can do this w/ the promise API is that you have multiple entry points for the success condition. You can still support people checking their own 4xx errors by doing await r2(url).response but when doing await r2(url).json you can add some additional default semantics that make sense 99% of the time.

The real power here isn't anything as simple as callbacks vs. promises. The real power is in the fact that errors throw, and that you can customize what is considered an error based on what property is accessed. This allows you to add all kinds of semantics for different usages that you just can't inspect when all you have is a callback to handle every class of error and success.

You don't need users to plug and propagate errors by hand, that's all language level now. And because you aren't passing in this future handler for the error you can create multiple entry points for the success conditions.

@jakearchibald
Copy link

jakearchibald commented Aug 31, 2017

@mikeal there's also response.ok which maps to what you wrote. https://fetch.spec.whatwg.org/#ref-for-dom-response-ok①.

Wait I'm being an idiot, that was the old example. Ignore me.

@darrentorpey
Copy link

Thanks for the detailed explanation, @mikeal -- interesting PoC

@mrpeu
Copy link

mrpeu commented Sep 1, 2017

I want to believe you; I feel like there is something powerful I'm missing. But I don't understand your answer to @bluepnume and @bahmutov.
@jakearchibald: I'll take over the idiot badge ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment