Skip to content

Commit

Permalink
feat: Allow following redirects
Browse files Browse the repository at this point in the history
* Allow consuming the main export as an object with static functions.
* Add option followRedirects (-L, --location).
* Enable test coverage.
  • Loading branch information
prantlf committed Dec 12, 2021
1 parent 5dbbff7 commit 0736037
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 105 deletions.
49 changes: 20 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The programmatic interface did not change and has remained compatible.

## Command-line usage

Make sure that you have [Node.js] >= 8 installed. Install the `nettime` package globally and print timings of a sample web site:
Make sure that you have [Node.js] >= 12 installed. Install the `nettime` package globally and print timings of a sample web site:

```bash
$ npm install -g nettime
Expand Down Expand Up @@ -59,6 +59,7 @@ Options:
-i, --include include response headers in the output
-I, --head use HEAD verb to get document info only
-k, --insecure ignore certificate errors
-L, --location follow redirects
-o, --output <file> write the received data to a file
-t, --time-unit <unit> set time unit: ms, s+ns
-u, --user <credentials> credentials for Basic Authentication
Expand All @@ -79,28 +80,33 @@ Examples:

## Programmatic usage

Make sure that you use [Node.js] >= 8. Install the `nettime` package locally and get time duration of waiting for the response and downloading the content of a sample web page:
Make sure that you use [Node.js] >= 12. Install the `nettime` package locally and get time duration of waiting for the response and downloading the content of a sample web page:

```bash
npm install --save nettime
```

```js
const nettime = require('nettime')
const { nettime, getDuration } = require('nettime')
nettime('https://www.google.com')
.then(result => {
if (result.statusCode === 200) {
let timings = result.timings
let waiting = nettime.getDuration([0, 0], timings.firstByte)
let downloading = nettime.getDuration(timings.firstByte, timings.contentTransfer)
let waiting = getDuration([0, 0], timings.firstByte)
let downloading = getDuration(timings.firstByte, timings.contentTransfer)
console.log('Waiting for the response:', nettime.getMilliseconds(waiting) + 'ms')
console.log('Downloading the content:', nettime.getMilliseconds(downloading) + 'ms')
}
})
.catch(error => console.error(error))
```

The main module exports a function which makes a HTTP/S request and returns a [Promise] to the result object.
The main module exports a function which makes a HTTP/S request and returns a [Promise] to the result object. The function carries properties `nettime`, `getDuration`, `getMilliseconds` and `isRedirect`, so that the export can be consumed as an object with several static functions too:

```js
const nettime = require('nettime')
const { nettime, getDuration } = require('nettime')
```

The input argument is a string with a URL to make the request with, or an object with multiple properties.

Expand All @@ -112,6 +118,7 @@ The input object can contain:
* `failOnOutputFileError`: boolean for preventing the request timing operation from failing, if writing to the output file failed. If set to `false`, the error will be printed on the standard output and the process exit code will be set to 2. It is in effect only if `outputFile` is specified. The default is `true`.
* `headers`: object with header names as string keys and header values as string values.
* `httpVersion`: string with the protocol version ('1.0', '1.1' or '2.0') to be sent to the server. (Node.js HTTP support is hard-coded for 1.1. There can be a difference between 1.0 and 1.1 on the server side only. Node.js supports HTTP/2 in the version 8.4.0 or newer with the --expose-http2 command-lime option and in the version 8.8.1 or newer out-of-the-box. Alternatively, you can install a "http2" module as a polyfill.)
* `followRedirects`: boolean to continue making requests, if the original response contained a redirecting HTTP status code
* `includeHeaders`: boolean for including property `headers` (`Object`) with response headers in the promised result object. If `outputFile` is specified, the headers are written to the beginning of the output file too.
* `method`: HTTP verb to use in the HTTP request; `GET` is the default, unless `-i` or `-d` options are not set.
* `outputFile`: file path to write the received data to.
Expand All @@ -127,6 +134,8 @@ The result object contains:
* `statusCode`: [HTTP status code] of the response (integer).
* `statusMessage`: HTTP status message for the status code (string).
* `timings`: object with timing properties from various stages of the request. Timing is an array with two integers - seconds and nanoseconds passed since the request has been made, as returned by [process.hrtime].
* `headers`: an optional object with the response headers, if enabled by the option `includeHeaders`.
* `url`: an optional string with the requested URL, if the option `followRedirects` was set to `true`.

```js
{
Expand All @@ -145,7 +154,8 @@ The result object contains:
}
```

If the option `requestCount` is greater than `1`, the result objects will be returned in an array of the same length as teh `requestCount` value.
If the option `requestCount` is greater than `1`, the result objects will be returned in an array of the same length as the `requestCount` value.
If the option `followRedirects` us set to `true`, the result objects will be returned in an array of the length depending on the presence and count of redirecting responses.

*Note*: The `time-unit` parameter affects not only the "text" output format of the command line script, but also the "json" one. If set to "ms", timing values will be printed in milliseconds. If set to "s+ns", timings will be printed as arrays in [process.hrtime]'s format. Calling the `nettime` function programmatically will always return the timings as arrays in [process.hrtime]'s format.

Expand All @@ -157,43 +167,24 @@ The following functions are exposed as named exports from the `nettime/lib/timin
* `getMilliseconds(timing)`: converts the timing to milliseconds. Expects an array in [process.hrtime]'s format and returns the result as an integer.
* `computeAverageDurations(multipleTimings)`: computes average durations from an array of event timings. The array is supposed to contain objects with the same keys as the `timings` object from the `nettime` response. The returned object will contain the same keys pointing to event durations in [process.hrtime]'s format.
* `createTimingsFromDurations(timings, startTime)`: reconstructs event timings from event durations. The `timings` object is supposed to contain the same keys as the `timings` object from the `nettime` response, but pointing to event durations in [process.hrtime]'s format. The returned object will contain the same keys, but pointing to event times in [process.hrtime]'s format. The `startTime` parameter can shoft the event times. The default is no shift - `[0, 0]`.
* `isRedirect(statusCode)`: checks if the HTTP status code means a redirect. Returns `true` if it is, otherwise `false`.

These methods can be required separately too:

```js
const { isRedirect } = require('nettime')
const {
getDuration, getMilliseconds,
computeAverageDurations, createTimingsFromDurations
} = require('nettime/lib/timings')
```

Methods `getDuration` and `getMilliseconds` are accessible also as static methods of the `nettime` function exported from the main `nettime` module.
Methods `getDuration`, `getMilliseconds` and `isRedirect` are accessible also as static methods of the `nettime` function exported from the main `nettime` module.

## Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using Grunt.

## Release History

* 2019-10-19 v3.0.0 Support multiple requests and average timings,
dropped support of Node.js 6
* 2019-10-18 v2.1.2 Fix crash on Node.js 10 and newer
* 2019-03-10 v2.1.0 Added option for setting connection timeout
* 2018-05-19 v2.0.1 Fixed http2 connection for Node.js 8.11.2
* 2018-04-27 v2.0.0 Dropped support of Node.js 4
* 2018-03-16 v1.1.2 Upgrade package dependencies
* 2017-12-21 v1.1.1 Upgrade semantic release and other dependencies
* 2017-11-11 v1.1.0 Support HTTP/2 requests
* 2017-11-06 v1.0.0 Make command-line options compatible with [curl]
* 2017-11-06 v0.5.0 Add support for the [curl] options "iIXdo"
* 2017-11-06 v0.4.0 Support custom headers and Basic Authentication
* 2017-11-05 v0.3.3 Do not add seconds in nanosecond precision to avoid errors
* 2017-11-04 v0.3.2 Print HTTP status message too
* 2017-10-22 v0.3.1 Round resulting milliseconds instead of truncating them
* 2017-10-22 v0.3.0 Allow ignoring of TLS certificate errors
* 2017-10-22 v0.2.0 Add timing for Socket Close
* 2017-10-21 v0.1.0 Initial release

## License

Copyright (c) 2017-2021 Ferdinand Prantl
Expand Down
77 changes: 71 additions & 6 deletions bin/nettime
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

const commander = require('commander')
const nettime = require('..')
const { nettime, isRedirect } = require('..')
const { version, description } = require('../package.json')
const {
computeAverageDurations, createTimingsFromDurations
Expand All @@ -23,6 +23,7 @@ commander
.option('-i, --include', 'include response headers in the output')
.option('-I, --head', 'use HEAD verb to get document info only')
.option('-k, --insecure', 'ignore certificate errors')
.option('-L, --location', 'follow redirects')
.option('-o, --output <file>', 'write the received data to a file')
.option('-t, --time-unit <unit>', 'set time unit: ms, s+ns')
.option('-u, --user <credentials>', 'credentials for Basic Authentication')
Expand Down Expand Up @@ -106,7 +107,8 @@ if (credentials) {

const {
connectTimeout: timeout, data, head, include: includeHeaders, insecure,
output: outputFile, request, requestCount, requestDelay, averageTimings
output: outputFile, request, requestCount, requestDelay, averageTimings,
location: followRedirects
} = options
const httpVersion = options.http2 ? '2.0' : options['http1.0'] ? '1.0' : '1.1'
const method = request || (head ? 'HEAD' : data ? 'POST' : 'GET')
Expand All @@ -126,21 +128,30 @@ nettime({
rejectUnauthorized,
timeout,
requestCount,
requestDelay
requestDelay,
followRedirects
})
.then(results => {
if (requestCount > 1) {
if (averageTimings) {
const result = computeAverageTimings(results)
results = [result]
if (followRedirects) {
results = computeRedirectableAverageTimings(results)
} else {
const result = computeAverageTimings(results)
results = [result]
}
}
} else {
} else if (!followRedirects) {
results = [results]
}
return results
})
.then(results => {
for (const result of results) {
if (followRedirects) {
console.log('URL:', result.url)
console.log()
}
console.log(formatter(result))
console.log()
}
Expand Down Expand Up @@ -191,3 +202,57 @@ function computeAverageTimings (results) {
return { timings, httpVersion, statusCode, statusMessage }
}
}

function computeRedirectableAverageTimings (results) {
checkStatusCodes()
const resultsByURL = collectResults()
const durationsByURL = collectAverageDurations()
return createAverageResult()

function checkStatusCodes () {
let firstStatusCode
for (const { statusCode } of results) {
if (isRedirect(statusCode)) continue
if (firstStatusCode === undefined) {
firstStatusCode = statusCode
} else {
if (firstStatusCode !== statusCode) {
throw new Error(`Status code of the first request was ${firstStatusCode}, but ${statusCode} was received later.`)
}
}
}
}

function collectResults () {
const resultsByURL = {}
for (const result of results) {
const { url } = result
const results = resultsByURL[url] || (resultsByURL[url] = [])
results.push(result)
}
return resultsByURL
}

function collectAverageDurations () {
const durationsByURL = {}
for (const url in resultsByURL) {
const timings = resultsByURL[url].map(({ timings }) => timings)
durationsByURL[url] = computeAverageDurations(timings)
}
return durationsByURL
}

function createAverageResult () {
const results = []
for (const url in resultsByURL) {
const result = extractResult(resultsByURL[url][0])
const timings = createTimingsFromDurations(durationsByURL[url])
results.push({ ...result, timings })
}
return results

function extractResult ({ url, httpVersion, statusCode, statusMessage }) {
return { url, httpVersion, statusCode, statusMessage }
}
}
}
Loading

0 comments on commit 0736037

Please sign in to comment.