770 lines
34 KiB
Markdown
770 lines
34 KiB
Markdown
---
|
|
title: 'Code Review: Axios'
|
|
date: 2020-10-07 22:49:11
|
|
tags:
|
|
- JavaScript
|
|
- Programming
|
|
- Code Review
|
|
---
|
|
|
|
> This article is in a series of code review articles that take a
|
|
> deep look at a popular module and discuss its merits, flaws, and
|
|
> overall fitness for a task.
|
|
|
|
## Summary
|
|
|
|
[Axios](https://github.com/axios/axios) is a solid, battle-tested, replacement for the deprecated `require.js`. I
|
|
recommend it despite the imperfections that I have summarized in this article.
|
|
|
|
The logic in this package is well thought out and meets my standards. Some of
|
|
the more complex functions are arduous to read. This makes collaboration from
|
|
the development community difficult and undermines the overall effectiveness of
|
|
the project.
|
|
|
|
The interceptor system is a workable solution for extending the package
|
|
functions. Personally, I have wrapped request/response to extend error reporting
|
|
and the updates felt natural and a seamless transition.
|
|
|
|
There are two test runners in the project: Mocha (for node.js) and Jasmine/Karma
|
|
(for browser testing). This is unnecessary as both test packages can run both
|
|
platforms. A large number of the tests are written for jasmine and will not
|
|
run, without modification in the mocha test suite. This prevents me from
|
|
showing full code coverage without hacking on the tests (more on this later).
|
|
|
|
Running `npm test` takes many minutes and fails, by default, if the developer
|
|
does not have the Opera browser installed. I can understand that an exhaustive
|
|
integration run in a multi-target package is a long process. Fleshing out the
|
|
Mocha test suite to run on the command line in a second npm script would
|
|
encourage test-driven refactoring and make many of the improvements I outline
|
|
much simpler and safer. Iterative, refactoring tests must be fast, sane, and
|
|
meaningful. They need not be exhaustive. The exhaustive testing can be saved
|
|
for pre-release and proofing pull-requests.
|
|
|
|
The current version is `0.20.0`. There is no explicit roadmap for the project;
|
|
however, I do not see a reason for the delay in assigning version `1.0.0` to this
|
|
release.
|
|
|
|
|
|
## About Reviewed Version & System
|
|
|
|
|
|
- Repository: https://github.com/axios/axios
|
|
- Reviewed Commit Hash: `6d05b96dcae6c82e28b049fce3d4d44e6d15a9bc`
|
|
- Average weekly downloads: 12 million
|
|
- Version: 0.20.0
|
|
- `du -sh dist`: 244K
|
|
- Dependencies: 1
|
|
- follow-redirects
|
|
- Node: v14.10.1
|
|
- npm: 4.16.8
|
|
- uname -a
|
|
- Linux morlock 5.4.0-7642-generic #46\~1598628707\~20.04\~040157c-Ubuntu x86\_64 GNU/Linux
|
|
|
|
Axios is a promise-based HTTP client. It is available for use in the
|
|
browser (wrapping around XMLHttpRequest) or in node.js (wrapping the
|
|
built-in `http` module.
|
|
|
|
## Setup
|
|
|
|
```
|
|
✔ pilot@morlock ~/Projects/codereview % git clone https://github.com/axios/axios.git
|
|
Cloning into 'axios'...
|
|
...
|
|
... <snip>
|
|
...
|
|
✔ pilot@morlock ~/Projects/codereview % npm install
|
|
|
|
... <snip> npm WARN deprecated 18 messages
|
|
...
|
|
|
|
> iltorb@2.4.5 install /home/pilot/Projects/codereview/axios/node_modules/iltorb
|
|
> node ./scripts/install.js || node-gyp rebuild
|
|
|
|
...
|
|
... <snip> Complation messages for node_modules/iltorb
|
|
... <snip> package postinstall garbage
|
|
...
|
|
|
|
npm notice created a lockfile as package-lock.json. You should commit this file.
|
|
npm WARN notsup Unsupported engine for karma@1.7.1: wanted: {"node":"0.10 || 0.12 || 4 || 5 || 6 || 7 || 8"} (current: {"node":"14.10.1","npm":"6.14.8"})
|
|
npm WARN notsup Not compatible with your version of node/npm: karma@1.7.1
|
|
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^1.2.7 (node_modules/chokidar/node_modules/fsevents):
|
|
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
|
|
npm WARN ajv-keywords@2.1.1 requires a peer of ajv@^5.0.0 but none is installed. You must install peer dependencies yourself.
|
|
|
|
added 975 packages from 1871 contributors and audited 978 packages in 61.395s
|
|
|
|
11 packages are looking for funding
|
|
run `npm fund` for details
|
|
|
|
found 33 vulnerabilities (22 low, 10 high, 1 critical)
|
|
run `npm audit fix` to fix them, or `npm audit` for details
|
|
```
|
|
|
|
Installed 975 packages. All but one are dev.
|
|
|
|
`axios` installs `bundlesize@^0.17.0`, which is a drop-in
|
|
replacement for `du -sh` (if `du` required oAuth read/write access to your
|
|
github account). Bundlesize uses a hand full of compression modules, including
|
|
`iltorb`. `iltorb` is deprecated garbage.
|
|
|
|
### NPM Audit Review
|
|
|
|
```
|
|
✔ pilot@morlock ~/Projects/codereview/axios % npm --no-color audit > audit.txt
|
|
```
|
|
|
|
I'm not going to detail all of the audit warnings, they are mostly from the
|
|
[`debug` package](https://github.com/visionmedia/debug/pull/504/files) and it's
|
|
[DDoS Regex](https://npmjs.com/advisories/534).
|
|
|
|
The first High level security threat is from installing the `karma` test runner:
|
|
|
|
```
|
|
✘ pilot@morlock ~/Projects/codereview/axios % npm ls ws
|
|
axios@0.20.0 /home/pilot/Projects/codereview/axios
|
|
└─┬ karma@1.7.1
|
|
└─┬ socket.io@1.7.3
|
|
├─┬ engine.io@1.8.3
|
|
│ └── ws@1.1.2
|
|
└─┬ socket.io-client@1.7.3
|
|
└─┬ engine.io-client@1.8.3
|
|
└── ws@1.1.2 deduped
|
|
```
|
|
|
|
That is an extremely old version of `ws`. [It has been
|
|
fixed](https://www.npmjs.com/advisories/550)
|
|
|
|
I love `lodash` for the creativity of its codebase, the way the developers
|
|
step up to the challenge of being faster than native, but never user it. NPM
|
|
awards `lodash`'s prototype pollution (actually polyfills) a `High` level alert.
|
|
|
|
`karma` also depends on an old version of `chokidar`, but [new chokidar is way
|
|
cooler](https://paulmillr.com/posts/chokidar-3-save-32tb-of-traffic/).
|
|
|
|
The `Critical` alert award goes to: `webpack-dev-server` ... [kind of a let
|
|
down](https://npmjs.com/advisories/725)
|
|
|
|
[`parsejson` is installed](https://npmjs.com/advisories/528)
|
|
|
|
I went into all the depth here because it is important to note that while all of
|
|
these dependencies are dev only, they are eliminated by just updating the
|
|
dependencies[^1]. This is a bad code smell and indicator of lazy development cycles.
|
|
|
|
|
|
## Running the tests
|
|
|
|
```
|
|
✘ pilot@morlock ~/Projects/codereview/axios % npm test
|
|
|
|
> axios@0.20.0 test /home/pilot/Projects/codereview/axios
|
|
> grunt test && bundlesize
|
|
|
|
Running "eslint:target" (eslint) task
|
|
|
|
Running "mochaTest:test" (mochaTest) task
|
|
|
|
|
|
...
|
|
... <snip> Test list
|
|
...
|
|
|
|
29 passing (730ms)
|
|
1 pending
|
|
|
|
|
|
Running "karma:single" (karma) task
|
|
Running locally since SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are not set.
|
|
(node:26309) Warning: Accessing non-existent property 'VERSION' of module exports inside circular dependency
|
|
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
Hash: f4683f5fa2953dc3a97c
|
|
Version: webpack 1.15.0
|
|
Time: 27ms
|
|
webpack: Compiled successfully.
|
|
webpack: Compiling...
|
|
webpack: wait until bundle finished:
|
|
<snip>
|
|
Hash: 17b067e2f01905fd51bf
|
|
Version: webpack 1.15.0
|
|
Time: 844ms
|
|
<snip>
|
|
|
|
webpack: Compiled successfully.
|
|
07 10 2020 23:56:29.288:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
|
|
07 10 2020 23:56:29.289:INFO [launcher]: Launching browsers Firefox, Chrome, Safari, Opera with unlimited concurrency
|
|
07 10 2020 23:56:29.294:INFO [launcher]: Starting browser Firefox
|
|
07 10 2020 23:56:29.311:INFO [launcher]: Starting browser Chrome
|
|
07 10 2020 23:56:29.328:INFO [launcher]: Starting browser Safari
|
|
07 10 2020 23:56:29.351:INFO [launcher]: Starting browser Opera
|
|
07 10 2020 23:56:29.388:ERROR [launcher]: No binary for Safari browser on your platform.
|
|
Please, set "SAFARI_BIN" env variable.
|
|
07 10 2020 23:56:32.140:INFO [Chrome 85.0.4183 (Linux 0.0.0)]: Connected on socket OZh8d5JZl6lqIyL8AAAA with id 4718835
|
|
................................................................................
|
|
................................................................................
|
|
................................................................................
|
|
......
|
|
Chrome 85.0.4183 (Linux 0.0.0): Executed 246 of 246 SUCCESS (2.522 secs / 2.462 secs)
|
|
07 10 2020 23:56:35.007:INFO [Firefox 81.0.0 (Ubuntu 0.0.0)]: Connected on socket -qEkpSILYreupv-OAAAB with id 87695166
|
|
................................................................................
|
|
................................................................................
|
|
................................................................................
|
|
......
|
|
Firefox 81.0.0 (Ubuntu 0.0.0): Executed 246 of 246 SUCCESS (2.512 secs / 2.455 secs)
|
|
|
|
08 10 2020 00:00:29.394:WARN [launcher]: Opera have not captured in 240000 ms, killing.
|
|
08 10 2020 00:00:31.398:WARN [launcher]: Opera was not killed in 2000 ms, sending SIGKILL.
|
|
08 10 2020 00:00:33.400:WARN [launcher]: Opera was not killed by SIGKILL in 2000 ms, continuing.
|
|
TOTAL: 492 SUCCESS
|
|
Warning: Task "karma:single" failed. Use --force to continue.
|
|
|
|
Aborted due to warnings.
|
|
npm ERR! Test failed. See above for more details.
|
|
```
|
|
|
|
All tests passed... Except for the Opera tests; but I don't have the opera
|
|
browser installed, nor does anyone else.
|
|
|
|
It is interesting to note that karma detected immediately that I do not have
|
|
Safari installed; but took over two minutes to not find Opera. This is an old
|
|
version of karma so I can not criticise.
|
|
|
|
One of the mocha tests is skipped: `should support sockets`. Setting this test to
|
|
run passes[^2].
|
|
|
|
## Code Coverage
|
|
|
|
This was not as straightforward as I thought it would be. The `package.json`
|
|
has an entry for `coveralls` but the `lcov` file wasn't generated by the test
|
|
run. Looking in `node_modules` I don't see an entry for `blanket.js`. *Another
|
|
bad smell.* Checking the `travis-ci` runs for the project they all error on `npm
|
|
run coveralls`.
|
|
|
|
Let's live dangerously:
|
|
|
|
```
|
|
✘ pilot@morlock ~/Projects/codereview/axios % npx nyc@latest npm test
|
|
npx: installed 141 in 5.972s
|
|
|
|
> axios@0.20.0 test /home/pilot/Projects/codereview/axios
|
|
> grunt test && bundlesize
|
|
|
|
Running "eslint:target" (eslint) task
|
|
|
|
Running "mochaTest:test" (mochaTest) task
|
|
...
|
|
... <snip> Test list
|
|
...
|
|
|
|
|
|
-------------------------|---------|----------|---------|---------|-----------------------------------
|
|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
-------------------------|---------|----------|---------|---------|-----------------------------------
|
|
All files | 79.21 | 62.78 | 79.44 | 79.73 |
|
|
axios | 33.33 | 11.36 | 40 | 34.69 |
|
|
Gruntfile.js | 43.75 | 0 | 50 | 50 | 92-101
|
|
index.js | 100 | 100 | 100 | 100 |
|
|
karma.conf.js | 26.47 | 11.9 | 33.33 | 26.47 | 7,20-109,111-115
|
|
axios/lib | 85.71 | 72.94 | 82.35 | 85.22 |
|
|
axios.js | 90.91 | 100 | 33.33 | 90.91 | 36,46
|
|
defaults.js | 88.57 | 82.14 | 100 | 88.57 | 20,44,47-48
|
|
utils.js | 82.26 | 68.42 | 83.33 | 81.03 | 73,95,130,190-214,235,241,280,284
|
|
axios/lib/adapters | 93.88 | 78.69 | 100 | 95.8 |
|
|
http.js | 93.88 | 78.69 | 100 | 95.8 | 42,46,93,118,122,164
|
|
axios/lib/cancel | 84.62 | 50 | 88.89 | 84.62 |
|
|
Cancel.js | 80 | 0 | 50 | 80 | 14
|
|
CancelToken.js | 84.21 | 50 | 100 | 84.21 | 13,25,38
|
|
isCancel.js | 100 | 100 | 100 | 100 |
|
|
axios/lib/core | 87.32 | 74.65 | 78.79 | 87.32 |
|
|
Axios.js | 75.68 | 56.25 | 66.67 | 75.68 | 31-32,42-45,53,57,68-69
|
|
InterceptorManager.js | 53.85 | 0 | 40 | 53.85 | 18-22,31-32,46-47
|
|
buildFullPath.js | 100 | 100 | 100 | 100 |
|
|
createError.js | 100 | 100 | 100 | 100 |
|
|
dispatchRequest.js | 95.65 | 68.75 | 100 | 95.65 | 69
|
|
enhanceError.js | 90 | 100 | 50 | 90 | 24
|
|
mergeConfig.js | 100 | 95.83 | 100 | 100 | 15
|
|
settle.js | 83.33 | 80 | 100 | 83.33 | 17
|
|
transformData.js | 100 | 100 | 100 | 100 |
|
|
axios/lib/helpers | 40.82 | 17.86 | 58.33 | 39.58 |
|
|
bind.js | 100 | 100 | 100 | 100 |
|
|
buildURL.js | 13.79 | 4.55 | 25 | 13.79 | 6,29-69
|
|
combineURLs.js | 100 | 50 | 100 | 100 | 11
|
|
isAbsoluteURL.js | 100 | 100 | 100 | 100 |
|
|
normalizeHeaderName.js | 66.67 | 75 | 100 | 66.67 | 8-9
|
|
spread.js | 33.33 | 100 | 0 | 33.33 | 24-25
|
|
-------------------------|---------|----------|---------|---------|-----------------------------------
|
|
```
|
|
|
|
Not bad for a zero-config nyc run.
|
|
|
|
[See the full report here](/~timemachine/codereview/axios@0.20.0/)
|
|
|
|
|
|
## Digging In
|
|
|
|
Now that all that boilerplate is out of the way let's look through the code.
|
|
File by file:
|
|
|
|
### [lib/axios.js](/~timemachine/codereview/axios@0.20.0/lib/axios.js.html)
|
|
|
|
The `package.json` lists the default entry point as `/index.js` but that just
|
|
exports `lib/axios.js`.
|
|
|
|
This is a general module building/exporting file. A default instance is created:
|
|
`axios` and then some other helpers are glued onto it.
|
|
|
|
1. axios: for the simple *requarian*, `cosnt axios = require('axios');`
|
|
2. axios.Axios: for *classical* `new Axios()` constructing.
|
|
3. axios.create: for the *[Crockfordian](https://crockford.com/javascript/prototypal.html)*.
|
|
|
|
The bizarre part of this is that all of the above give you different results.
|
|
The requarian form is constructed with a set of defaults from
|
|
`lib/defaults.js`; the classical constructor has no defaults;
|
|
`axios.create` merges the supplied configuration with the defaults.
|
|
|
|
|
|
### [lib/defaults.js](/~timemachine/codereview/axios@0.20.0/lib/defaults.js.html)
|
|
|
|
Simple and sane defaults. However, the
|
|
[`getDefaultAdapter()`](/~timemachine/codereview/axios@0.20.0/lib/defaults.js.html#L16)
|
|
function allows me to point out one of the stinkiest code
|
|
smells: *All if … else if constructs shall be terminated with an else clause.*
|
|
(See MISRA-C:2004, Rule 14.10, no online links, sorry).
|
|
|
|
This function should be terminated with an `else { throw new Error('Axios does
|
|
not support this platform') }` clause. That uncaught fall though leaves the
|
|
default adapter as `undefined` and the application in an unknown state.
|
|
|
|
#### Dynamic Imports
|
|
|
|
While picking on `getDefaultAdapter()`, there are two synchronous `require`
|
|
calls. First, I have to say that putting requires calls deep in function logic is
|
|
a red flag; don't do it, ever. Secondly, it's fine to do it here...
|
|
|
|
Let me explain by assessing the 3 potential code paths, from the bottom up:
|
|
|
|
1. The default path: As I explained above the default path is to just return
|
|
`undefined`. No harm from the synchronous calls.
|
|
2. Browsers: In the context of the browser the `require` function is provided by webpack. The function is synchronous; however, webpack barfs all the javascript assets into memory on page load. Thus, require,
|
|
in this context doesn't result in a bad turn[^3].
|
|
3. Node.js: [node's module
|
|
resolution](https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L717)
|
|
uses a caching system that only reads the file once from disk, the first time
|
|
it is encountered. All other requests to `require` for the same file will
|
|
return the object that was previously loaded. This creates a large number of
|
|
blocking turns early in the application life cycle, but as long as you keep
|
|
all of the require statements up at the top level of your file the turns will
|
|
smoothen out once all the files are sourced.
|
|
|
|
This seems like a contradiction. However, the call to `getDefaultAdapter()`
|
|
*is* at the top level, when the file loads. We can observe that it is the same
|
|
event loop turn that loads the other requires in this file (`utils` and
|
|
`normalizeHeaderName`) and the call to load the http adapter.
|
|
|
|
My final verdict: this is a *good* example of multi-platform, dynamic
|
|
requirement retrieval in CommonJS. ES Modules also have [dynamic
|
|
imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports).
|
|
However, the `import()` function in ESM is asynchronous so refactoring this
|
|
package to a module will require some additional state management.
|
|
|
|
#### [`transformResponse`](/~timemachine/codereview/axios@0.20.0/lib/defaults.js.html#L57)
|
|
|
|
`transformResponse` has a laughable JSON.parse() usage that should really be
|
|
sniffing the content-type header to avoid parsing XML/HTML as JSON.
|
|
|
|
### [lib/utils.js](/~timemachine/codereview/axios@0.20.0/lib/utils.js.html)
|
|
|
|
The bulk of this file is `is*` duck-typing functions. These are great helpers
|
|
for when you don't know the browser you are targeting. I'm happy to see these
|
|
in the package as opposed to another dependency.
|
|
|
|
Some *utils* exist natively across all the supported browsers and should be
|
|
removed or wrap the native functions:
|
|
|
|
- Array.isArray()
|
|
- Array.prototype.forEach()
|
|
- String.prototype.trim() which also removes: `\uFEFF` and `\xA0`, ~LMFAO~.
|
|
|
|
Don't copy `isNumber`, it's not the best way to do that. There is no best way to
|
|
write `isNumber` in JavaScript so you should consider the usefulness of
|
|
`Number.isFinite()`, `Number.isNaN()`, and (if you expect absolute values over 9
|
|
quadrillion) `Number.isSafeNumber()`.
|
|
|
|
My last thought on this file is not a knock on axios. It is a knock on Unicode,
|
|
and it's byte-order-mark. [They must
|
|
go](/~timemachine/codereview/axios@0.20.0/lib/utils.js.html#L315). I do
|
|
recommend that the `stripBOM` function assert it was passed a string before
|
|
modifying it.
|
|
|
|
|
|
### lib/helpers/
|
|
|
|
I'm confused. There is a `lib/utils.js` and a `lib/helpers/*`. Another sign of
|
|
copy/pasta? ... Let's dig in.
|
|
|
|
#### [lib/helpers/bind.js](/~timemachine/codereview/axios@0.20.0/lib/helpers/bind.js.html)
|
|
|
|
"Clean your ears out and listen close sonny ... Back in my day, before
|
|
JavaScript had *splats* we had this mysterious thing called the `arguments`
|
|
array. We never knew where it came from but it was always there. And even if I
|
|
called it an *array* it wasn't really an array but more like an object, with
|
|
numerical indexes..."
|
|
|
|
Targeting old browsers is the pits. It forces you to make hack helper functions
|
|
like bind. A necessary evil for pre-IE 9 support[^4].
|
|
|
|
Fortunately, Axios doesn't care about browsers that old. This should go.
|
|
|
|
#### [lib/helpers/buildURL.js](/~timemachine/codereview/axios@0.20.0/lib/helpers/buildURL.js.html)
|
|
|
|
This file looks great ... Except maybe this line: `replace(/%20/g, '+').` ...
|
|
Whatever floats ya boat.
|
|
|
|
|
|
#### [lib/helpers/combineURLs.js](/~timemachine/codereview/axios@0.20.0/lib/helpers/combineURLs.js.html)
|
|
|
|
Needs to check that `baseURL` is defined (and a string) before calling
|
|
`.replace()` on it.
|
|
|
|
#### [lib/helpers/isAbsoluteURL.js](/~timemachine/codereview/axios@0.20.0/lib/helpers/isAbsoluteURL.js.html)
|
|
|
|
If you take the time to read the Regex it follows the spec. I shy away from
|
|
lengthy Regular Expressions. The logic is hard to test, hard to debug, and prone
|
|
to misinterpreted readings by other developers.
|
|
|
|
#### [lib/helpers/normalizeHeaderName.js](/~timemachine/codereview/axios@0.20.0/lib/helpers/normalizeHeaderName.js.html)
|
|
|
|
This function takes an object of `[key: string]: string` properties and a second
|
|
argument as a key. It then changes the spelling of the key in the first argument to
|
|
match the capitalization of the second argument.
|
|
|
|
All header keys in HTTP should be treated as lower case. A more sensible action is
|
|
to take the input headers and convert them all to lowercase. Then use the
|
|
lowercase forms in all interactions. Better still: create an
|
|
enumeration of the headers Axios cared about and then only use references to the
|
|
enumeration.
|
|
|
|
#### [lib/helpers/spread.js](/~timemachine/codereview/axios@0.20.0/lib/helpers/spread.js.html)
|
|
|
|
If you are a JavaScript novice, I can only explain the function in
|
|
`./helpers/spread.js` by saying: "It allows you to not type `null` when using
|
|
Function.prototype.apply()."
|
|
|
|
The documentation states this function is deprecated. It should log a warning
|
|
when called.
|
|
|
|
### lib/adapters
|
|
|
|
To allow interoperability between Browser and Node.js the *Adapter* pattern is used, or maybe it's the *Abstract Factory* pattern. ([But, what's in a name?](https://martinfowler.com/bliki/TwoHardThings.html)).
|
|
|
|
According to the [Gang of Four](https://en.wikipedia.org/wiki/Design_Patterns)
|
|
use an Abstract Factory to:
|
|
|
|
> Provide an interface for creating families of related or dependant objects
|
|
> without specifying their concrete classes.
|
|
|
|
The
|
|
[README.md](https://github.com/axios/axios/tree/6d05b96dcae6c82e28b049fce3d4d44e6d15a9bc/lib/adapters)
|
|
does a great job of defining the contract for all adapters to agree.
|
|
|
|
#### [lib/adapters/http.js](/~timemachine/codereview/axios@0.20.0/lib/adapters/http.js.html)
|
|
|
|
I'll start my critique by pointing out: [The second edition of Martin
|
|
Fowler's "Refactoring"](https://martinfowler.com/articles/refactoring-2nd-ed.html)
|
|
uses JavaScript for its examples.
|
|
|
|
The cyclomatic complexity of `httpAdapter` is 42, according to JSHint. That's
|
|
just too much. There are some easy wins here for refactoring:
|
|
|
|
- Creating a new function for converting POST data to a Buffer drops the complexity by six. The resulting function is easier to test.
|
|
- Transforming the `config.auth` object and jamming it into the URL totals
|
|
*ten* paths. Not as easy to refactor as there are some overlapping concerns:
|
|
- resolve the `username` and `password`
|
|
- parse the URL
|
|
- ~coalesce~ ignore the previous `username` and `password` if the URL has
|
|
its own
|
|
- continue to use the parsed URL over the next 90 lines
|
|
- Proxy configuration is complex; 60 lines and 13 paths.
|
|
- Finally, the callback to `transport.request()` should be it's own function,
|
|
and possibly in its own file.
|
|
|
|
#### [lib/adapters/xhr.js](https://github.com/axios/axios/blob/6d05b96dcae6c82e28b049fce3d4d44e6d15a9bc/lib/adapters/xhr.js)
|
|
|
|
Another grizzly adapter file. *Twenty-three* cyclomatic complexity value. I'll
|
|
avoid the exhaustive refactoring analysis but will mention there is some clean
|
|
up of the user/password logic that should be plucked from these files and into a
|
|
helper.
|
|
|
|
I just can't nit-pick this file as much. Feature sniffing the version of
|
|
XMLHttpRequest is complex, by definition.
|
|
|
|
### lib/cancel
|
|
|
|
Axios reimplements [cancel-able
|
|
promises](https://github.com/axios/axios#cancellation). I'm most familiar with
|
|
[Bluebird.js's Cancellations](http://bluebirdjs.com/docs/api/cancellation.html).
|
|
|
|
Since Bluebird is providing its own definition of Promises it can safely augment
|
|
a Promise with a `.cancel()` function. Axios does not have this luxury, making
|
|
the cancellation functionality more complex.
|
|
|
|
#### [lib/cancel/Cancel.js](/~timemachine/codereview/axios@0.20.0/lib/cancel/Cancel.js.html)
|
|
|
|
A prototype-based class with two properties that mimics the `Error` class,
|
|
in JavaScript.
|
|
|
|
- `message`: A description of why the cancellation happened.
|
|
- `__CANCEL__`: Always true. Used to duck-type Cancel objects from Error
|
|
objects.
|
|
|
|
I would not mind seeing this extend the `Error` class. The added features of
|
|
`Error` (specifically the stack trace) could come in handy.
|
|
Additionally/Alternatively the constructor could take a `reason` parameter that
|
|
is an instance of an error.
|
|
|
|
|
|
#### [lib/cancel/CancelToken.js](/~timemachine/codereview/axios@0.20.0/lib/cancel/CancelToken.js.html)
|
|
|
|
A CancelToken is an identifier that triggers the cancellation process.
|
|
|
|
The class has a factory to create a `source` and a constructor that does not
|
|
return a `source`. Instead, it returns a function. This is confusing. I
|
|
recommend:
|
|
|
|
- `CancelToken` should return a `source`. This would unify the two styles of
|
|
cancellation. However it would also break backward-compatibility.
|
|
- Fix compatibility by making the source returned by the factory a function that
|
|
can be called directly. Annotate the source code of why this sloppiness
|
|
is present.
|
|
- Optionally: warn about deprecation when `source` is called as a function.
|
|
(there would be different styles of warning for Browser vs. Node.js so
|
|
feasibility is debatable)
|
|
- Update the documentation to only use the `source` style cancellation.
|
|
|
|
### lib/core
|
|
|
|
Now that we have dissected the supporting characters, we can dive into the heart
|
|
of the matter.
|
|
|
|
#### [lib/core/transformData.js](/~timemachine/codereview/axios@0.20.0/lib/core/transformData.js.html)
|
|
|
|
This is a helper (should be moved to that folder?) to loop over the transform
|
|
configuration for the Requests and Responses.
|
|
|
|
The first line of the function is an eslint suppression comment. Manipulating
|
|
the config of static analysis tools at run time is sometimes needed; however, it
|
|
should be the exception and accompanied by a large amount of explanation
|
|
comments.
|
|
|
|
It's such a short function I'll just show you how this function should be
|
|
changed. I'll also rename the ambiguous variable names to ease my sanity:
|
|
|
|
```
|
|
module.exports = function(data, headers, transforms) {
|
|
var result = data;
|
|
utils.foreach(transforms, function (transform) {
|
|
result = transform(result, headers);
|
|
});
|
|
return result;
|
|
};
|
|
```
|
|
|
|
In this trivial function, the usefulness of the `no-param-reassign` seems
|
|
suspect. Function bodies should treat all parameters as immutable, never assign
|
|
new values to them and it is absolutely crucial to never perform property
|
|
reassignment to parameters. Function purity is one of the best defenses we have
|
|
against defects.
|
|
|
|
As I said, this small function is easy to understand and there is little
|
|
chance of a bug finding its way in here, but practicing proper habits when the
|
|
complexity is low sets us up for success when tackling the 42 headed hydra of
|
|
`lib/adapters/http.js`.
|
|
|
|
#### The error resolution tango
|
|
|
|
I'll explain the next few files as a group.
|
|
|
|
- [lib/core/settle.js](/~timemachine/codereview/axios@0.20.0/lib/core/settle.js.html)
|
|
- [lib/core/createError.js](/~timemachine/codereview/axios@0.20.0/lib/core/createError.js.html)
|
|
- [lib/core/enhanceError.js](/~timemachine/codereview/axios@0.20.0/lib/core/enhanceError.js.html)
|
|
|
|
The two adapters (xml and http) both pass their responses along with the promise
|
|
callbacks to `settle`. Passing resolve/reject to a function is a bit thick.
|
|
Settle can just return a promise if it needs asynchrony (it doesn't).
|
|
|
|
Settle looks for the existence of the dubiously named: `validateStatus`
|
|
function and calls it to determine if the response was a success or error.
|
|
`validateStatus` takes one argument: the HTTP Status Code of the response. By
|
|
default a response is deemed a failure if its status code is between 200 and
|
|
299 (inclusive).
|
|
|
|
I think this `validateStatus` stuff is all *Feature-Request-Duct-Tape* and not
|
|
well thought out. Here is my suggestion: pass the whole response to
|
|
`validateStatus` as a second argument.
|
|
|
|
If `validateStatus` deems the response a failure, the response is passed (as
|
|
constituent parts) to `createError`.
|
|
|
|
`createError` is correctly named as an error factory. It instantiates a new
|
|
Error object and then passes the error along with it's the other arguments to
|
|
`enhanceError`.
|
|
|
|
`enhanceError` adds the request config, response code, complete request, complete
|
|
response, and a new property (`isAxiosError`) to the error object before doing
|
|
something that is seemingly bizarre: it arguments the error with a new function
|
|
called, `toJSON()` that just makes a copy of its self...But why[^6]?
|
|
|
|
Take this example from the node REPL:
|
|
|
|
```
|
|
> JSON.stringify(new Error('bad thing'));
|
|
'{}'
|
|
```
|
|
|
|
Yep. You can't encode an `Error` object as JSON...*sadface*...Luckily, JSON
|
|
provides a canonical way of tackling the problems of JavaScript. The `stringify`
|
|
function inspects the objects (recursively) for a `toJSON` method. If found the
|
|
result of calling `toJSON` is encoded in place of the parent object:
|
|
|
|
```
|
|
> const e = new Error('bad thing');
|
|
undefined
|
|
> e.toJSON = function () { return this.name + ': ' + this.message; }
|
|
[Function (anonymous)]
|
|
> JSON.stringify(e);
|
|
'"Error: bad thing"'
|
|
```
|
|
|
|
You can see how node.js [handles calling `console.log` on an error](https://github.com/nodejs/node/blob/70834250e83fa89e92314be37a9592978ee8c6bd/lib/internal/util/inspect.js#L1176),
|
|
and other edge cases [deep in the bowels of
|
|
`formatRaw`](https://github.com/nodejs/node/blob/70834250e83fa89e92314be37a9592978ee8c6bd/lib/internal/util/inspect.js#L806).
|
|
|
|
Looking at the locations there `createError` and `enhanceError` are used through
|
|
the code base, I can stomach their existence. One problem I have, generally,
|
|
with Error factories is they botch the stack trace.
|
|
|
|
#### [lib/core/buildFullPath.js](/~timemachine/codereview/axios@0.20.0/lib/core/buildFullPath.js.html)
|
|
|
|
[This looks familiar](#lib-helpers-combineURLs-js)
|
|
|
|
Let's compare:
|
|
|
|
- buildFullPath:
|
|
|
|
```
|
|
module.exports = function buildFullPath(baseURL, requestedURL) {
|
|
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
|
return combineURLs(baseURL, requestedURL);
|
|
}
|
|
return requestedURL;
|
|
};
|
|
```
|
|
|
|
- combineURLs:
|
|
|
|
```
|
|
module.exports = function combineURLs(baseURL, relativeURL) {
|
|
return relativeURL
|
|
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
|
|
: baseURL;
|
|
};
|
|
```
|
|
|
|
A quick search of the source code and the only usage of `combineURLs` is *in*
|
|
`buildFullPath`. In-line it!
|
|
|
|
Also, rename `buildFullPath`, it is too similar to
|
|
[`buildURL`](#lib-helpers-buildURL-js).
|
|
|
|
|
|
#### [lib/core/mergeConfig.js](/~timemachine/codereview/axios@0.20.0/lib/core/mergeConfig.js.html)
|
|
|
|
Gonna pull out the *yellow card*. Up to this point I have seen much-a-do about
|
|
supporting IE 6, 7, and 8. But here we see calls to `Object.keys` which pins the
|
|
compatibility to IE 9+.
|
|
|
|
(Editor's Note: the README only claims compatibility for IE 11+)
|
|
|
|
Cloning objects is needlessly hard in JavaScript (how many versions before
|
|
`Object.prototype.clone()`? ... it's possible). If you need such arcane
|
|
transformations study the [MDN page on the subject](https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).
|
|
|
|
Honestly, if you can't `Object.assign(a, JSON.parse(JSON.stringify(b)));` what's
|
|
the point of living? (no, that doesn't work)
|
|
|
|
#### [lib/core/dispatchRequest.js](/~timemachine/codereview/axios@0.20.0/lib/core/dispatchRequest.js.html)
|
|
|
|
These files are getting *meaty*. There is a nice example of a [cross-cutting
|
|
concern](https://en.wikipedia.org/wiki/Cross-cutting_concern) here. Everyplace
|
|
where there may be a new turn in the event loop a check to
|
|
`throwIfCancellationRequested` shows up.
|
|
|
|
Remember when I said that [you get different configurations depending on how you
|
|
instantiate the `axios` object](#lib-axios-js)? The bug materializes into [line
|
|
50](/~timemachine/codereview/axios@0.20.0/lib/core/dispatchRequest.js.html#L50).
|
|
|
|
#### [lib/core/InterceptorManager.js](/~timemachine/codereview/axios@0.20.0/lib/core/InterceptorManager.js.html)
|
|
|
|
I could fan-boy all over the InterceptorManager all day. This class allows the
|
|
developer to decorate (add pre/post hooks to) the request. We will see this in
|
|
action in Axios.js.
|
|
|
|
#### [lib/core/Axios.js](/~timemachine/codereview/axios@0.20.0/lib/core/Axios.js.html)
|
|
|
|
Shout out to the power of prototypal inheritance: Lines 73 - 93.
|
|
|
|
The
|
|
[constructor](/~timemachine/codereview/axios@0.20.0/lib/core/Axios.js.html#L9)
|
|
makes the config accessible from the incorrectly named `defaults` property. Then
|
|
sets up the interceptors.
|
|
|
|
You can see the *Chain of Responsibility Pattern*, as a literal `chain`
|
|
variable. The chain is initialized with the dispatcher and undefined (you need
|
|
an even number of links in the chain, because: *shenanigans*). All of the
|
|
pre-request interceptors are un-shifted to the start of the chain (even numbers
|
|
again for a fulfilled or rejected promise). Likewise of the post-request
|
|
interceptors are pushed to the end of the chain. Finally, two of the
|
|
interceptor-callbacks are removed from the head of the chain and added as
|
|
*thenables* to a master promise (one for fulfilled and one for rejected). In
|
|
the case of the dispatcher it handles its own rejection so we need an extra
|
|
element in the chain *shenanigans!*.
|
|
|
|
The master promise resolves all the pre-request hooks, the dispatcher, and all
|
|
the post-request hooks in order. **Study this code until you understand how it
|
|
works!**
|
|
|
|
## In Closing
|
|
|
|
I use the Axios library, professionally, to send business-critical requests to
|
|
third-party servers. When software is critical to your business you must be
|
|
critical of the software. However, the majority of software available through
|
|
the Node Package Manager passes without scrutiny.
|
|
|
|
From the length of this article, you can guess that this was a multi-week effort
|
|
to type up all my thoughts. I did as much research on my claims as I thought was
|
|
reasonable to give an accurate representation. The first draft was not perfect
|
|
and I have made revisions as my understanding of the source code evolved.
|
|
|
|
While passionate about code quality I also have a sense of humor; my hope is
|
|
that both aspects enrich the reading experience and that my wit does not
|
|
distract the reader.
|
|
|
|
Thank you to my proofreaders and technical editors, without them this would be
|
|
awful.
|
|
|
|
I have included additional action items in some of the footnotes and will update
|
|
them with links to any pull requests that result so that the reader can stay
|
|
abreast of my contributions.
|
|
|
|
[^1]: TODO: Update dependants; open PR.
|
|
[^2]: TODO: Pull request, re-adding the skipped test.
|
|
[^3]: A *bad turn* is when the browser/server/application's event loop becomes
|
|
blocked. When blocked the application will become unresponsive for some period
|
|
of time until the block clears.
|
|
[^4]: Roughly 1.0% of Browsers support XMLHttpRequest but not
|
|
Function.prototype.bind.
|
|
[^5]: *Feature-Request-Duct-Tape* is a quick code fix-up to fill a feature
|
|
request in the easiest possible way. Feature-Request-Duct-Tape usually smells
|
|
of configuration based feature flags, copy-pasta, `if...return` blocks, and
|
|
unrelated `elseif` predicates.
|
|
[^6]: TODO: Turn this in to a post with an example of `Error.prototype.toJSON()`
|