Repository: mcollina/autocannon
Pull request #239 (2020-01-24 09:56:20)

feat: add setupRequest per request for requests array
Hey all,

Since the `setupClient` appears to mutate the `Client` classs located in `httpClient.js`, I decided to add the ability to mutate each request independently within the `requests` array if it is provided. 

I thought about using the `setupClient` property at first, but it appears that each request in the `requests` array is passed to the `RequestIterator` and is never constructed as a `Client` presumably for performance reasons. For this purpose, I decided to name a new property called `setupRequest` that mutates the raw request object.

I have added corresponding tests, documentation and have followed the code style in autocannon. I hope this is a meaningful addition, as I myself did require this functionality in one of my projects where I tested the creation of entities and their subsequent mutation via a url param.

Please let me know if I should change anything in the PR to match your expectations.

mcollina: Good work on the feature, the code seems ok for me.
alex-ppg: Done, let me know if anything else is needed.

Patch:
diff --git a/.gitignore b/.gitignore
index 5cbaec6..526c682 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,9 @@ build/Release
 # Dependency directory
 node_modules
 
+# Dependency lock
+package-lock.json
+
 # Optional npm cache directory
 .npm
 
diff --git a/README.md b/README.md
index cd21522..f5b25d3 100644
--- a/README.md
+++ b/README.md
@@ -233,7 +233,7 @@ Start autocannon against the given target.
     * `connectionRate`: A `Number` stating the rate of requests to make per second from each individual connection. No rate limiting by default. _OPTIONAL_
     * `overallRate`: A `Number` stating the rate of requests to make per second from all connections. `connectionRate` takes precedence if both are set. No rate limiting by default. _OPTIONAL_
     * `reconnectRate`: A `Number` which makes the individual connections disconnect and reconnect to the server whenever it has sent that number of requests. _OPTIONAL_
-    * `requests`: An `Array` of `Object`s which represents the sequence of requests to make while benchmarking. Can be used in conjunction with the `body`, `headers` and `method` params above. The `Object`s in this array can have `body`, `headers`, `method`, or `path` attributes, which overwrite those that are passed in this `opts` object. Therefore, the ones in this (`opts`) object take precedence and should be viewed as defaults. Check the samples folder for an example of how this might be used. _OPTIONAL_.
+    * `requests`: An `Array` of `Object`s which represents the sequence of requests to make while benchmarking. Can be used in conjunction with the `body`, `headers` and `method` params above. The `Object`s in this array can have `body`, `headers`, `method`, or `path` attributes, which overwrite those that are passed in this `opts` object. Therefore, the ones in this (`opts`) object take precedence and should be viewed as defaults. Additionally, an optional `setupRequest` `Function` may be provided that will mutate the raw `request` object, e.g. `request.method = 'GET'`. Check the samples folder for an example of how this might be used. _OPTIONAL_.
     * `idReplacement`: A `Boolean` which enables the replacement of `[<id>]` tags within the request body with a randomly generated ID, allowing for unique fields to be sent with requests. Check out [an example of programmatic usage](./samples/using-id-replacement.js) can be found in the samples. _OPTIONAL_ default: `false`
     * `forever`: A `Boolean` which allows you to setup an instance of autocannon that restarts indefinitely after emiting results with the `done` event. Useful for efficiently restarting your instance. To stop running forever, you must cause a `SIGINT` or call the `.stop()` function on your instance. _OPTIONAL_ default: `false`
     * `servername`: A `String` identifying the server name for the SNI (Server Name Indication) TLS extension. _OPTIONAL_ default: `undefined`.
diff --git a/lib/httpRequestBuilder.js b/lib/httpRequestBuilder.js
index 8ccb589..d075f5f 100644
--- a/lib/httpRequestBuilder.js
+++ b/lib/httpRequestBuilder.js
@@ -13,6 +13,7 @@ function requestBuilder (defaults) {
     headers: {},
     body: Buffer.alloc(0),
     hostname: 'localhost',
+    setupRequest: reqData => reqData,
     port: 80
   }
 
@@ -27,6 +28,9 @@ function requestBuilder (defaults) {
     reqData.headers = Object.assign({}, defaults.headers, reqData.headers)
 
     reqData = Object.assign({}, defaults, reqData)
+
+    reqData = reqData.setupRequest(reqData)
+
     // for some reason some tests fail with method === undefined
     // the reqData.method should be set to SOMETHING in this case
     // cannot find reason for failure if `|| 'GET'` is taken out
diff --git a/samples/requests-sample.js b/samples/requests-sample.js
index 4b77719..7f447f3 100644
--- a/samples/requests-sample.js
+++ b/samples/requests-sample.js
@@ -37,7 +37,7 @@ function startBench () {
         // this will automatically add the pregenerated auth token
       },
       {
-        method: 'PUT', // this should be a put for modifying secret details
+        method: 'GET', // this should be a put for modifying secret details
         path: '/mySecretDetails',
         headers: { // let submit some json?
           'Content-type': 'application/json; charset=utf-8'
@@ -45,7 +45,11 @@ function startBench () {
         // we need to stringify the json first
         body: JSON.stringify({
           name: 'my new name'
-        })
+        }),
+        setupRequest: reqData => {
+          reqData.method = 'PUT' // we are overriding the method 'GET' to 'PUT' here
+          return reqData
+        }
       }
     ]
   }, finishedBench)


Test patch:
diff --git a/test/requestIterator.test.js b/test/requestIterator.test.js
index 9e4c325..dc3d59e 100644
--- a/test/requestIterator.test.js
+++ b/test/requestIterator.test.js
@@ -187,3 +187,51 @@ test('request iterator should replace all [<id>] tags with generated IDs when ca
   t.equal(result.includes('[<id>]'), false, 'One or more [<id>] tags were not replaced')
   t.equal(result.slice(-1), '0', 'Generated ID should end with request number')
 })
+
+test('request iterator should properly mutate requests if a setupRequest function is located', (t) => {
+  t.plan(6)
+
+  const opts = server.address()
+  opts.method = 'POST'
+
+  let i = 0
+
+  const requests1 = [
+    {
+      body: 'hello world',
+      setupRequest: req => {
+        req.body += i++
+        return req
+      }
+    },
+    {
+      method: 'POST',
+      body: 'modified',
+      setupRequest: req => {
+        req.method = 'GET'
+        return req
+      }
+    }
+  ]
+
+  const request1Res = Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 12\r\n\r\nhello world0`)
+  const request2Res = Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 9\r\n\r\nmodified1`)
+  const request3Res = Buffer.from(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 8\r\n\r\nmodified`)
+  const request4Res = Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nheader: modifiedHeader\r\nContent-Length: 9\r\n\r\nmodified2`)
+  const request5Res = Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`)
+
+  opts.requests = requests1
+
+  const iterator = new RequestIterator(opts)
+  t.same(iterator.currentRequest.requestBuffer, request1Res, 'request was okay')
+  iterator.setBody('modified')
+  t.same(iterator.currentRequest.requestBuffer, request2Res, 'request was okay')
+  iterator.nextRequest() // verify it didn't affect the other request
+  t.same(iterator.currentRequest.requestBuffer, request3Res, 'request was okay')
+  iterator.nextRequest()
+  t.same(iterator.currentRequest.requestBuffer, request2Res, 'request was okay')
+  iterator.setHeaders({ header: 'modifiedHeader' })
+  t.same(iterator.currentRequest.requestBuffer, request4Res, 'request was okay')
+  iterator.setRequest() // this should build default request
+  t.same(iterator.currentRequest.requestBuffer, request5Res, 'request was okay')
+})