Repository: nock/nock
Pull request #2017 (2020-06-28 16:13:51)

Upgrade 13.0.0 to latest
13.0.0 was published under `next` a couple weeks ago and [tweeted out](https://twitter.com/nodenock/status/1272614295990415362).

No issues have come up as far as I know. I personally, have v13 running in a few dozen CIs for enterprise apps without issue.

ref: https://github.com/nock/nock/pull/1989


Patch:
diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml
index 610c678..c734af4 100644
--- a/.github/workflows/continuous-integration.yaml
+++ b/.github/workflows/continuous-integration.yaml
@@ -161,5 +161,5 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        node-version: [10, 12, 13]
+        node-version: [10, 12, 13, 14]
         os: [macos-latest, ubuntu-latest, windows-latest]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02f73c0..b37be7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
 # Changelog
 
-Nock’s changelog can be found directly in the [GitHub release notes](https://github.com/nock/nock/releases). These are automatically created by [semantic-release](https://github.com/semantic-release/semantic-release) based on their [commit message conventions](https://semantic-release.gitbook.io/semantic-release#commit-message-format).
+Nock’s changelog can be found directly in the [GitHub release notes](https://github.com/nock/nock/releases).  
+These are automatically created by [semantic-release](https://github.com/semantic-release/semantic-release) based on their [commit message conventions](https://semantic-release.gitbook.io/semantic-release#commit-message-format).
+
+Migration guides are available for major versions in the [migration guides directory](https://github.com/nock/nock/tree/master/migration_guides).
diff --git a/README.md b/README.md
index f5163c6..6b4ed04 100644
--- a/README.md
+++ b/README.md
@@ -43,10 +43,11 @@ For instance, if a module performs HTTP requests to a CouchDB server or makes HT
   - [Support for HTTP and HTTPS](#support-for-http-and-https)
   - [Non-standard ports](#non-standard-ports)
   - [Repeat response n times](#repeat-response-n-times)
-  - [Delay the response body](#delay-the-response-body)
   - [Delay the response](#delay-the-response)
-  - [Delay the connection](#delay-the-connection)
-  - [Socket timeout](#socket-timeout)
+    - [Delay the connection](#delay-the-connection)
+      - [Technical Details](#technical-details)
+    - [Delay the response body](#delay-the-response-body)
+      - [Technical Details](#technical-details-1)
   - [Chaining](#chaining)
   - [Scope filtering](#scope-filtering)
   - [Conditional scope filtering](#conditional-scope-filtering)
@@ -63,7 +64,6 @@ For instance, if a module performs HTTP requests to a CouchDB server or makes HT
   - [.pendingMocks()](#pendingmocks)
   - [.activeMocks()](#activemocks)
   - [.isActive()](#isactive)
-- [Logging](#logging)
 - [Restoring](#restoring)
 - [Activating](#activating)
 - [Turning Nock Off (experimental!)](#turning-nock-off-experimental)
@@ -141,8 +141,8 @@ const scope = nock('https://api.github.com')
       name: 'MIT License',
       spdx_id: 'MIT',
       url: 'https://api.github.com/licenses/mit',
-      node_id: 'MDc6TGljZW5zZTEz'
-    }
+      node_id: 'MDc6TGljZW5zZTEz',
+    },
   })
 ```
 
@@ -261,9 +261,7 @@ nock('http://www.example.com')
 Nock understands query strings. Search parameters can be included as part of the path:
 
 ```js
-nock('http://example.com')
-  .get('/users?foo=bar')
-  .reply(200)
+nock('http://example.com').get('/users?foo=bar').reply(200)
 ```
 
 Instead of placing the entire URL, you can specify the query part as an object:
@@ -284,8 +282,8 @@ nock('http://example.com')
     names: ['alice', 'bob'],
     tags: {
       alice: ['admin', 'tester'],
-      bob: ['tester']
-    }
+      bob: ['tester'],
+    },
   })
   .reply(200, { results: [{ id: 'pgte' }] })
 ```
@@ -295,10 +293,7 @@ A `URLSearchParams` instance can be provided.
 ```js
 const params = new URLSearchParams({ foo: 'bar' })
 
-nock('http://example.com')
-  .get('/')
-  .query(params)
-  .reply(200)
+nock('http://example.com').get('/').query(params).reply(200)
 ```
 
 Nock supports passing a function to query. The function determines if the actual query matches or not.
@@ -339,9 +334,7 @@ nock('http://example.com', { encodedQueryParams: true })
 You can specify the return status code for a path on the first argument of reply like this:
 
 ```js
-const scope = nock('http://myapp.iriscouch.com')
-  .get('/users/1')
-  .reply(404)
+const scope = nock('http://myapp.iriscouch.com').get('/users/1').reply(404)
 ```
 
 You can also specify the reply body as a string:
@@ -355,13 +348,11 @@ const scope = nock('http://www.google.com')
 or as a JSON-encoded object:
 
 ```js
-const scope = nock('http://myapp.iriscouch.com')
-  .get('/')
-  .reply(200, {
-    username: 'pgte',
-    email: 'pedro.teixeira@gmail.com',
-    _id: '4324243fsd'
-  })
+const scope = nock('http://myapp.iriscouch.com').get('/').reply(200, {
+  username: 'pgte',
+  email: 'pedro.teixeira@gmail.com',
+  _id: '4324243fsd',
+})
 ```
 
 or even as a file:
@@ -370,7 +361,7 @@ or even as a file:
 const scope = nock('http://myapp.iriscouch.com')
   .get('/')
   .replyWithFile(200, __dirname + '/replies/user.json', {
-    'Content-Type': 'application/json'
+    'Content-Type': 'application/json',
   })
 ```
 
@@ -412,7 +403,7 @@ const scope = nock('http://www.google.com')
     return [
       201,
       'THIS IS THE REPLY BODY',
-      { header: 'value' } // optional headers
+      { header: 'value' }, // optional headers
     ]
   })
 ```
@@ -444,7 +435,7 @@ If you're using the reply callback style, you can access the original client req
 ```js
 const scope = nock('http://www.google.com')
   .get('/cat-poems')
-  .reply(function(uri, requestBody) {
+  .reply(function (uri, requestBody) {
     console.log('path:', this.req.path)
     console.log('headers:', this.req.headers)
     // ...
@@ -466,12 +457,10 @@ nock('http://www.google.com')
 JSON error responses are allowed too:
 
 ```js
-nock('http://www.google.com')
-  .get('/cat-poems')
-  .replyWithError({
-    message: 'something awful happened',
-    code: 'AWFUL_ERROR'
-  })
+nock('http://www.google.com').get('/cat-poems').replyWithError({
+  message: 'something awful happened',
+  code: 'AWFUL_ERROR',
+})
 ```
 
 > Note: This will emit an `error` event on the `request` object, not the reply.
@@ -489,8 +478,8 @@ You can specify the request headers like this:
 ```js
 const scope = nock('http://www.example.com', {
   reqheaders: {
-    authorization: 'Basic Auth'
-  }
+    authorization: 'Basic Auth',
+  },
 })
   .get('/')
   .reply(200)
@@ -503,8 +492,8 @@ function will be passed the header value.
 const scope = nock('http://www.example.com', {
   reqheaders: {
     'X-My-Headers': headerValue => headerValue.includes('cats'),
-    'X-My-Awesome-Header': /Awesome/i
-  }
+    'X-My-Awesome-Header': /Awesome/i,
+  },
 })
   .get('/')
   .reply(200)
@@ -518,7 +507,7 @@ You can also have Nock fail the request if certain headers are present:
 
 ```js
 const scope = nock('http://www.example.com', {
-  badheaders: ['cookie', 'x-forwarded-for']
+  badheaders: ['cookie', 'x-forwarded-for'],
 })
   .get('/')
   .reply(200)
@@ -554,7 +543,7 @@ const scope = nock('http://www.headdy.com')
   .get('/')
   .reply(200, 'Hello World!', {
     'Content-Length': (req, res, body) => body.length,
-    ETag: () => `${Date.now()}`
+    ETag: () => `${Date.now()}`,
   })
 ```
 
@@ -566,7 +555,7 @@ You can also specify default reply headers for all responses like this:
 const scope = nock('http://www.headdy.com')
   .defaultReplyHeaders({
     'X-Powered-By': 'Rails',
-    'Content-Type': 'application/json'
+    'Content-Type': 'application/json',
   })
   .get('/')
   .reply(200, 'The default headers should come too')
@@ -577,7 +566,7 @@ Or you can use a function to generate the default headers values:
 ```js
 const scope = nock('http://www.headdy.com')
   .defaultReplyHeaders({
-    'Content-Length': (req, res, body) => body.length
+    'Content-Length': (req, res, body) => body.length,
   })
   .get('/')
   .reply(200, 'The default headers should come too')
@@ -653,10 +642,7 @@ const scope = nock('http://my.server.com:8081')
 You are able to specify the number of times to repeat the same response.
 
 ```js
-nock('http://zombo.com')
-  .get('/')
-  .times(4)
-  .reply(200, 'Ok')
+nock('http://zombo.com').get('/').times(4).reply(200, 'Ok')
 
 http.get('http://zombo.com/') // respond body "Ok"
 http.get('http://zombo.com/') // respond body "Ok"
@@ -668,38 +654,16 @@ http.get('http://zombo.com/') // respond with zombo.com result
 Sugar syntax
 
 ```js
-nock('http://zombo.com')
-  .get('/')
-  .once()
-  .reply(200, 'Ok')
-nock('http://zombo.com')
-  .get('/')
-  .twice()
-  .reply(200, 'Ok')
-nock('http://zombo.com')
-  .get('/')
-  .thrice()
-  .reply(200, 'Ok')
+nock('http://zombo.com').get('/').once().reply(200, 'Ok')
+nock('http://zombo.com').get('/').twice().reply(200, 'Ok')
+nock('http://zombo.com').get('/').thrice().reply(200, 'Ok')
 ```
 
 To repeat this response for as long as nock is active, use [.persist()](#persist).
 
-### Delay the response body
-
-You are able to specify the number of milliseconds that the response body should be delayed. Response header will be replied immediately.
-`delayBody(1000)` is equivalent to `delay({body: 1000})`.
-
-```js
-nock('http://my.server.com')
-  .get('/')
-  .delayBody(2000) // 2 seconds
-  .reply(200, '<html></html>')
-```
-
-NOTE: the [`'response'`](http://nodejs.org/api/http.html#http_event_response) event will occur immediately, but the [IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) will not emit its `'end'` event until after the delay.
-
 ### Delay the response
 
+Nock can simulate response latency to allow you to test timeouts, race conditions, an other timing related scenarios.  
 You are able to specify the number of milliseconds that your reply should be delayed.
 
 ```js
@@ -709,53 +673,54 @@ nock('http://my.server.com')
   .reply(200, '<html></html>')
 ```
 
-`delay()` could also be used as
+`delay(1000)` is an alias for `delayConnection(1000).delayBody(0)`  
+`delay({ head: 1000, body: 2000 })` is an alias for `delayConnection(1000).delayBody(2000)`  
+Both of which are covered in detail below.
 
-```js
-delay({
-  head: headDelayInMs,
-  body: bodyDelayInMs
-})
-```
+#### Delay the connection
 
-for example
+You are able to specify the number of milliseconds that your connection should be idle before it starts to receive the response.
+
+To simulate a socket timeout, provide a larger value than the timeout setting on the request.
 
 ```js
 nock('http://my.server.com')
   .get('/')
-  .delay({
-    head: 2000, // header will be delayed for 2 seconds, i.e. the whole response will be delayed for 2 seconds.
-    body: 3000 // body will be delayed for another 3 seconds after header is sent out.
-  })
+  .delayConnection(2000) // 2 seconds
   .reply(200, '<html></html>')
+
+req = http.request('http://my.server.com', { timeout: 1000 })
 ```
 
-### Delay the connection
+Nock emits timeout events almost immediately by comparing the requested connection delay to the timeout parameter passed to `http.request()` or `http.ClientRequest#setTimeout()`.  
+This allows you to test timeouts without using fake timers or slowing down your tests.
+If the client chooses to _not_ take an action (e.g. abort the request), the request and response will continue on as normal, after real clock time has passed.
+
+##### Technical Details
+
+Following the `'finish'` event being emitted by `ClientRequest`, Nock will wait for the next event loop iteration before checking if the request has been aborted.
+At this point, any connection delay value is compared against any request timeout setting and a [`'timeout'`](https://nodejs.org/api/http.html#http_event_timeout) is emitted when appropriate from the socket and the request objects.
+A Node timeout timer is then registered with any connection delay value to delay real time before checking again if the request has been aborted and the [`'response'`](http://nodejs.org/api/http.html#http_event_response) is emitted by the request.
 
-`delayConnection(1000)` is equivalent to `delay({ head: 1000 })`.
+A similar method, `.socketDelay()` was removed in version 13. It was thought that having two methods so subtlety similar was confusing.  
+The discussion can be found at https://github.com/nock/nock/pull/1974.
 
-### Socket timeout
+#### Delay the response body
 
-You are able to specify the number of milliseconds that your connection should be idle, to simulate a socket timeout.
+You are able to specify the number of milliseconds that the response body should be delayed.  
+This is the time between the headers being received and the body starting to be received.
 
 ```js
 nock('http://my.server.com')
   .get('/')
-  .socketDelay(2000) // 2 seconds
+  .delayBody(2000) // 2 seconds
   .reply(200, '<html></html>')
 ```
 
-To test a request like the following:
+##### Technical Details
 
-```js
-req = http.request('http://my.server.com', res => {
-  ...
-})
-req.setTimeout(1000, () => { req.abort() })
-req.end()
-```
-
-NOTE: the timeout will be fired immediately, and will not leave the simulated connection idle for the specified period of time.
+Following the [`'response'`](http://nodejs.org/api/http.html#http_event_response) being emitted by `ClientRequest`,
+Nock will register a timeout timer with the body delay value to delay real time before the [IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) emits its first `'data'` or the `'end'` event.
 
 ### Chaining
 
@@ -767,19 +732,19 @@ const scope = nock('http://myapp.iriscouch.com')
   .reply(404)
   .post('/users', {
     username: 'pgte',
-    email: 'pedro.teixeira@gmail.com'
+    email: 'pedro.teixeira@gmail.com',
   })
   .reply(201, {
     ok: true,
     id: '123ABC',
-    rev: '946B7D1C'
+    rev: '946B7D1C',
   })
   .get('/users/123ABC')
   .reply(200, {
     _id: '123ABC',
     _rev: '946B7D1C',
     username: 'pgte',
-    email: 'pedro.teixeira@gmail.com'
+    email: 'pedro.teixeira@gmail.com',
   })
 ```
 
@@ -791,7 +756,7 @@ This can be useful if you have a node module that randomly changes subdomains to
 
 ```js
 const scope = nock('https://api.dropbox.com', {
-  filteringScope: scope => /^https:\/\/api[0-9]*.dropbox.com/.test(scope)
+  filteringScope: scope => /^https:\/\/api[0-9]*.dropbox.com/.test(scope),
 })
   .get('/1/metadata/auto/Photos?include_deleted=false&list=true')
   .reply(200)
@@ -805,7 +770,7 @@ This can be useful if you only want certain scopes to apply depending on how you
 
 ```js
 const scope = nock('https://api.myservice.com', {
-  conditionally: () => true
+  conditionally: () => true,
 })
 ```
 
@@ -876,7 +841,7 @@ const scope = nock('http://api.myservice.com')
   .matchHeader('accept', 'application/json')
   .get('/')
   .reply(200, {
-    data: 'hello world'
+    data: 'hello world',
   })
 ```
 
@@ -887,7 +852,7 @@ const scope = nock('http://api.myservice.com')
   .matchHeader('User-Agent', /Mozilla\/.*/)
   .get('/')
   .reply(200, {
-    data: 'hello world'
+    data: 'hello world',
   })
 ```
 
@@ -898,7 +863,7 @@ const scope = nock('http://api.myservice.com')
   .matchHeader('content-length', val => val >= 1000)
   .get('/')
   .reply(200, {
-    data: 'hello world'
+    data: 'hello world',
   })
 ```
 
@@ -915,20 +880,14 @@ example.pendingMocks() // ["GET http://example.com:80/path"]
 // ...After a request to example.com/pathA:
 example.pendingMocks() // []
 
-example
-  .get('/pathB')
-  .optionally()
-  .reply(200)
+example.get('/pathB').optionally().reply(200)
 example.pendingMocks() // []
 
 // You can also pass a boolean argument to `optionally()`. This
 // is useful if you want to conditionally make a mocked request
 // optional.
 const getMock = optional =>
-  example
-    .get('/pathC')
-    .optionally(optional)
-    .reply(200)
+  example.get('/pathC').optionally(optional).reply(200)
 
 getMock(true)
 example.pendingMocks() // []
@@ -978,9 +937,7 @@ setTimeout(() => {
 You can call `isDone()` on a single expectation to determine if the expectation was met:
 
 ```js
-const scope = nock('http://google.com')
-  .get('/')
-  .reply(200)
+const scope = nock('http://google.com').get('/').reply(200)
 
 scope.isDone() // will return false
 ```
@@ -1023,10 +980,7 @@ Note that while a persisted scope will always intercept the requests, it is cons
 If you want to stop persisting an individual persisted mock you can call `persist(false)`:
 
 ```js
-const scope = nock('http://example.com')
-  .persist()
-  .get('/')
-  .reply(200, 'ok')
+const scope = nock('http://example.com').persist().get('/').reply(200, 'ok')
 
 // Do some tests ...
 
@@ -1082,16 +1036,6 @@ if (!nock.isActive()) {
 }
 ```
 
-## Logging
-
-Nock can log matches if you pass in a log function like this:
-
-```js
-const scope = nock('http://google.com')
-                .log(console.log)
-                ...
-```
-
 ## Restoring
 
 You can restore the HTTP interceptor to the normal unmocked behaviour by calling:
@@ -1222,7 +1166,7 @@ If you just want to capture the generated code into a var as an array you can us
 
 ```js
 nock.recorder.rec({
-  dont_print: true
+  dont_print: true,
 })
 // ... some HTTP calls
 const nockCalls = nock.recorder.play()
@@ -1240,7 +1184,7 @@ In case you want to generate the code yourself or use the test data in some othe
 
 ```js
 nock.recorder.rec({
-  output_objects: true
+  output_objects: true,
 })
 // ... some HTTP calls
 const nockCallObjects = nock.recorder.play()
@@ -1261,7 +1205,7 @@ If you save this as a JSON file, you can load them directly through `nock.load(p
 
 ```js
 nocks = nock.load(pathToJson)
-nocks.forEach(function(nock) {
+nocks.forEach(function (nock) {
   nock.filteringRequestBody = (body, aRecordedBody) => {
     if (typeof body !== 'string' || typeof aRecordedBody !== 'string') {
       return body
@@ -1270,7 +1214,11 @@ nocks.forEach(function(nock) {
     const recordedBodyResult = /timestamp:([0-9]+)/.exec(aRecordedBody)
     if (recordedBodyResult) {
       const recordedTimestamp = recordedBodyResult[1]
-      return body.replace(/(timestamp):([0-9]+)/g, function(match, key, value) {
+      return body.replace(/(timestamp):([0-9]+)/g, function (
+        match,
+        key,
+        value
+      ) {
         return key + ':' + recordedTimestamp
       })
     } else {
@@ -1292,7 +1240,7 @@ nockDefs.forEach(def => {
   //  Do something with the definition object e.g. scope filtering.
   def.options = {
     ...def.options,
-    filteringScope: scope => /^https:\/\/api[0-9]*.dropbox.com/.test(scope)
+    filteringScope: scope => /^https:\/\/api[0-9]*.dropbox.com/.test(scope),
   }
 })
 
@@ -1310,7 +1258,7 @@ The genuine use cases for recording request headers (e.g. checking authorization
 nock.recorder.rec({
   dont_print: true,
   output_objects: true,
-  enable_reqheaders_recording: true
+  enable_reqheaders_recording: true,
 })
 ```
 
@@ -1325,7 +1273,7 @@ const appendLogToFile = content => {
   fs.appendFile('record.txt', content)
 }
 nock.recorder.rec({
-  logging: appendLogToFile
+  logging: appendLogToFile,
 })
 ```
 
@@ -1337,7 +1285,7 @@ To disable this, set `use_separator` to false.
 
 ```js
 nock.recorder.rec({
-  use_separator: false
+  use_separator: false,
 })
 ```
 
@@ -1350,7 +1298,7 @@ Examples:
 ```js
 nock.removeInterceptor({
   hostname: 'localhost',
-  path: '/mockedResource'
+  path: '/mockedResource',
 })
 ```
 
@@ -1426,7 +1374,7 @@ nockBack('zomboFixture.json', nockDone => {
     nockDone()
 
     // usage of the created fixture
-    nockBack('zomboFixture.json', function(nockDone) {
+    nockBack('zomboFixture.json', function (nockDone) {
       http.get('http://zombo.com/').end() // respond body "Ok"
 
       this.assertScopesFinished() //throws an exception if all nocks in fixture were not satisfied
@@ -1578,8 +1526,26 @@ It does this by manipulating the modules cache of Node in a way that conflicts w
 
 Nock uses [`debug`](https://github.com/visionmedia/debug), so just run with environmental variable `DEBUG` set to `nock.*`.
 
+```console
+user@local$ DEBUG=nock.* node my_test.js
+```
+
+Each step in the matching process is logged this way and can be useful when determining why a request was not intercepted by Nock.
+
+For example the following shows that matching failed because the request had an extra search parameter.
+
 ```js
-$ DEBUG=nock.* node my_test.js
+nock('http://example.com').get('/').query({ foo: 'bar' }).reply()
+
+await got('http://example.com/?foo=bar&baz=foz')
+```
+
+```console
+user@local$ DEBUG=nock.scope:example.com node my_test.js
+...
+nock.scope:example.com Interceptor queries: {"foo":"bar"} +1ms
+nock.scope:example.com     Request queries: {"foo":"bar","baz":"foz"} +0ms
+nock.scope:example.com query matching failed +0ms
 ```
 
 ## Contributing
diff --git a/lib/common.js b/lib/common.js
index 7679c0a..259c6e2 100644
--- a/lib/common.js
+++ b/lib/common.js
@@ -1,9 +1,10 @@
 'use strict'
 
-const _ = require('lodash')
 const debug = require('debug')('nock.common')
-const url = require('url')
+const set = require('lodash.set')
 const timers = require('timers')
+const url = require('url')
+const util = require('util')
 
 /**
  * Normalizes the request options so that it always has `host` property.
@@ -194,7 +195,7 @@ function isJSONContent(headers) {
  * Duplicates throw an error.
  */
 function headersFieldNamesToLowerCase(headers) {
-  if (!_.isPlainObject(headers)) {
+  if (!isPlainObject(headers)) {
     throw Error('Headers must be provided as an object')
   }
 
@@ -243,11 +244,11 @@ function headersInputToRawArray(headers) {
   }
 
   // [].concat(...) is used instead of Array.flat until v11 is the minimum Node version
-  if (_.isMap(headers)) {
+  if (util.types.isMap(headers)) {
     return [].concat(...Array.from(headers, ([k, v]) => [k.toString(), v]))
   }
 
-  if (_.isPlainObject(headers)) {
+  if (isPlainObject(headers)) {
     return [].concat(...Object.entries(headers))
   }
 
@@ -359,7 +360,7 @@ function addHeaderLine(headers, name, value) {
  * @fieldName {String} field name - string with the case-insensitive field name
  */
 function deleteHeadersField(headers, fieldNameToDelete) {
-  if (!_.isPlainObject(headers)) {
+  if (!isPlainObject(headers)) {
     throw Error('headers must be an object')
   }
 
@@ -559,7 +560,7 @@ const dataEqual = (expected, actual) =>
  * { 'foo[bar][0]': 'baz' } -> { foo: { bar: [ 'baz' ] } }
  */
 const expand = input =>
-  Object.entries(input).reduce((acc, [k, v]) => _.set(acc, k, v), {})
+  Object.entries(input).reduce((acc, [k, v]) => set(acc, k, v), {})
 
 /**
  * Performs a recursive strict comparison between two values.
@@ -572,7 +573,7 @@ function deepEqual(expected, actual) {
     return expected.test(actual)
   }
 
-  if (Array.isArray(expected) || _.isPlainObject(expected)) {
+  if (Array.isArray(expected) || isPlainObject(expected)) {
     if (actual === undefined) {
       return false
     }
@@ -588,6 +589,51 @@ function deepEqual(expected, actual) {
   return expected === actual
 }
 
+/**
+ * Checks if `value` is a plain object, that is, an object created by the
+ * `Object` constructor or one with a `[[Prototype]]` of `null`.
+ * https://github.com/lodash/lodash/blob/588bf3e20db0ae039a822a14a8fa238c5b298e65/isPlainObject.js
+ *
+ * @param {*} value The value to check.
+ * @return {boolean}
+ */
+function isPlainObject(value) {
+  const isObjectLike = typeof value === 'object' && value !== null
+  const tag = Object.prototype.toString.call(value)
+  if (!isObjectLike || tag !== '[object Object]') {
+    return false
+  }
+  if (Object.getPrototypeOf(value) === null) {
+    return true
+  }
+  let proto = value
+  while (Object.getPrototypeOf(proto) !== null) {
+    proto = Object.getPrototypeOf(proto)
+  }
+  return Object.getPrototypeOf(value) === proto
+}
+
+/**
+ * Creates an object with the same keys as `object` and values generated
+ * by running each own enumerable string keyed property of `object` thru
+ * `iteratee`. (iteration order is not guaranteed)
+ * The iteratee is invoked with three arguments: (value, key, object).
+ * https://github.com/lodash/lodash/blob/588bf3e20db0ae039a822a14a8fa238c5b298e65/mapValue.js
+ *
+ * @param {Object} object The object to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @returns {Object} Returns the new mapped object.
+ */
+function mapValue(object, iteratee) {
+  object = Object(object)
+  const result = {}
+
+  Object.keys(object).forEach(key => {
+    result[key] = iteratee(object[key], key, object)
+  })
+  return result
+}
+
 const timeouts = []
 const intervals = []
 const immediates = []
@@ -614,29 +660,33 @@ function removeAllTimers() {
   clearTimer(clearImmediate, immediates)
 }
 
-exports.normalizeClientRequestArgs = normalizeClientRequestArgs
-exports.normalizeRequestOptions = normalizeRequestOptions
-exports.normalizeOrigin = normalizeOrigin
-exports.isUtf8Representable = isUtf8Representable
-exports.overrideRequests = overrideRequests
-exports.restoreOverriddenRequests = restoreOverriddenRequests
-exports.stringifyRequest = stringifyRequest
-exports.isContentEncoded = isContentEncoded
-exports.contentEncoding = contentEncoding
-exports.isJSONContent = isJSONContent
-exports.headersFieldNamesToLowerCase = headersFieldNamesToLowerCase
-exports.headersFieldsArrayToLowerCase = headersFieldsArrayToLowerCase
-exports.headersArrayToObject = headersArrayToObject
-exports.headersInputToRawArray = headersInputToRawArray
-exports.deleteHeadersField = deleteHeadersField
-exports.forEachHeader = forEachHeader
-exports.percentEncode = percentEncode
-exports.percentDecode = percentDecode
-exports.matchStringOrRegexp = matchStringOrRegexp
-exports.formatQueryValue = formatQueryValue
-exports.isStream = isStream
-exports.dataEqual = dataEqual
-exports.setTimeout = setTimeout
-exports.setInterval = setInterval
-exports.setImmediate = setImmediate
-exports.removeAllTimers = removeAllTimers
+module.exports = {
+  contentEncoding,
+  dataEqual,
+  deleteHeadersField,
+  forEachHeader,
+  formatQueryValue,
+  headersArrayToObject,
+  headersFieldNamesToLowerCase,
+  headersFieldsArrayToLowerCase,
+  headersInputToRawArray,
+  isContentEncoded,
+  isJSONContent,
+  isPlainObject,
+  isStream,
+  isUtf8Representable,
+  mapValue,
+  matchStringOrRegexp,
+  normalizeClientRequestArgs,
+  normalizeOrigin,
+  normalizeRequestOptions,
+  overrideRequests,
+  percentDecode,
+  percentEncode,
+  removeAllTimers,
+  restoreOverriddenRequests,
+  setImmediate,
+  setInterval,
+  setTimeout,
+  stringifyRequest,
+}
diff --git a/lib/delayed_body.js b/lib/delayed_body.js
deleted file mode 100644
index 20359dd..0000000
--- a/lib/delayed_body.js
+++ /dev/null
@@ -1,52 +0,0 @@
-'use strict'
-
-/**
- * Creates a stream which becomes the response body of the interceptor when a
- * delay is set. The stream outputs the intended body and EOF after the delay.
- *
- * @param  {String|Buffer|Stream} body - the body to write/pipe out
- * @param  {Integer} ms - The delay in milliseconds
- * @constructor
- */
-
-const { Transform } = require('stream')
-const common = require('./common')
-
-module.exports = class DelayedBody extends Transform {
-  constructor(ms, body) {
-    super()
-
-    const self = this
-    let data = ''
-    let ended = false
-
-    if (common.isStream(body)) {
-      body.on('data', function (chunk) {
-        data += Buffer.isBuffer(chunk) ? chunk.toString() : chunk
-      })
-
-      body.once('end', function () {
-        ended = true
-      })
-
-      body.resume()
-    }
-
-    // TODO: This would be more readable if the stream case were moved into
-    // the `if` statement above.
-    common.setTimeout(function () {
-      if (common.isStream(body) && !ended) {
-        body.once('end', function () {
-          self.end(data)
-        })
-      } else {
-        self.end(data || body)
-      }
-    }, ms)
-  }
-
-  _transform(chunk, encoding, cb) {
-    this.push(chunk)
-    process.nextTick(cb)
-  }
-}
diff --git a/lib/intercept.js b/lib/intercept.js
index a98577f..a95eb51 100644
--- a/lib/intercept.js
+++ b/lib/intercept.js
@@ -159,7 +159,7 @@ function interceptorsFor(options) {
       // If scope filtering function is defined and returns a truthy value then
       // we have to treat this as a match.
       if (filteringScope && filteringScope(basePath)) {
-        debug('found matching scope interceptor')
+        interceptor.scope.logger('found matching scope interceptor')
 
         // Keep the filtered scope (its key) to signal the rest of the module
         // that this wasn't an exact but filtered match.
@@ -281,6 +281,8 @@ function overrideClientRequest() {
       debug('using', interceptors.length, 'interceptors')
 
       //  Use filtered interceptors to intercept requests.
+      // TODO: this shouldn't be a class anymore
+      // the overrider explicitly overrides methods and attrs on the request so the `assign` below should be removed.
       const overrider = new InterceptedRequestRouter({
         req: this,
         options,
diff --git a/lib/intercepted_request_router.js b/lib/intercepted_request_router.js
index babf4f6..0d0ede5 100644
--- a/lib/intercepted_request_router.js
+++ b/lib/intercepted_request_router.js
@@ -13,6 +13,20 @@ const globalEmitter = require('./global_emitter')
 const Socket = require('./socket')
 const { playbackInterceptor } = require('./playback_interceptor')
 
+function socketOnClose(req) {
+  debug('socket close')
+
+  if (!req.res && !req.socket._hadError) {
+    // If we don't have a response then we know that the socket
+    // ended prematurely and we need to emit an error on the request.
+    req.socket._hadError = true
+    const err = new Error('socket hang up')
+    err.code = 'ECONNRESET'
+    req.emit('error', err)
+  }
+  req.emit('close')
+}
+
 /**
  * Given a group of interceptors, appropriately route an outgoing request.
  * Identify which interceptor ought to respond, if any, then delegate to
@@ -39,16 +53,24 @@ class InterceptedRequestRouter {
     }
 
     this.response = new IncomingMessage(this.socket)
-    this.playbackStarted = false
     this.requestBodyBuffers = []
+    this.playbackStarted = false
+
+    // For parity with Node, it's important the socket event is emitted before we begin playback.
+    // This flag is set when playback is triggered if we haven't yet gotten the
+    // socket event to indicate that playback should start as soon as it comes in.
+    this.readyToStartPlaybackOnSocketEvent = false
 
     this.attachToReq()
+
+    // Emit a fake socket event on the next tick to mimic what would happen on a real request.
+    // Some clients listen for a 'socket' event to be emitted before calling end(),
+    // which causes Nock to hang.
+    process.nextTick(() => this.connectSocket())
   }
 
   attachToReq() {
-    const { req, response, socket, options } = this
-
-    response.req = req
+    const { req, options } = this
 
     for (const [name, val] of Object.entries(options.headers)) {
       req.setHeader(name.toLowerCase(), val)
@@ -65,18 +87,9 @@ class InterceptedRequestRouter {
     req.path = options.path
     req.method = options.method
 
-    // ClientRequest.connection is an alias for ClientRequest.socket
-    // https://nodejs.org/api/http.html#http_request_socket
-    // https://github.com/nodejs/node/blob/b0f75818f39ed4e6bd80eb7c4010c1daf5823ef7/lib/_http_client.js#L640-L641
-    // The same Socket is shared between the request and response to mimic native behavior.
-    req.socket = req.connection = socket
-
-    propagate(['error', 'timeout'], req.socket, req)
-
     req.write = (...args) => this.handleWrite(...args)
     req.end = (...args) => this.handleEnd(...args)
     req.flushHeaders = (...args) => this.handleFlushHeaders(...args)
-    req.abort = (...args) => this.handleAbort(...args)
 
     // https://github.com/nock/nock/issues/256
     if (options.headers.expect === '100-continue') {
@@ -85,50 +98,77 @@ class InterceptedRequestRouter {
         req.emit('continue')
       })
     }
+  }
 
-    // Emit a fake socket event on the next tick to mimic what would happen on a real request.
-    // Some clients listen for a 'socket' event to be emitted before calling end(),
-    // which causes nock to hang.
-    process.nextTick(() => {
-      req.emit('socket', socket)
+  connectSocket() {
+    const { req, socket } = this
 
-      // https://nodejs.org/api/net.html#net_event_connect
-      socket.emit('connect')
+    // Until Node 14 is the minimum, we need to look at both flags to see if the request has been cancelled.
+    if (req.destroyed || req.aborted) {
+      return
+    }
 
-      // https://nodejs.org/api/tls.html#tls_event_secureconnect
-      if (socket.authorized) {
-        socket.emit('secureConnect')
-      }
-    })
-  }
+    // ClientRequest.connection is an alias for ClientRequest.socket
+    // https://nodejs.org/api/http.html#http_request_socket
+    // https://github.com/nodejs/node/blob/b0f75818f39ed4e6bd80eb7c4010c1daf5823ef7/lib/_http_client.js#L640-L641
+    // The same Socket is shared between the request and response to mimic native behavior.
+    req.socket = req.connection = socket
 
-  emitError(error) {
-    const { req } = this
-    process.nextTick(() => {
-      req.emit('error', error)
-    })
+    propagate(['error', 'timeout'], socket, req)
+    socket.on('close', () => socketOnClose(req))
+
+    socket.connecting = false
+    req.emit('socket', socket)
+
+    // https://nodejs.org/api/net.html#net_event_connect
+    socket.emit('connect')
+
+    // https://nodejs.org/api/tls.html#tls_event_secureconnect
+    if (socket.authorized) {
+      socket.emit('secureConnect')
+    }
+
+    if (this.readyToStartPlaybackOnSocketEvent) {
+      this.maybeStartPlayback()
+    }
   }
 
+  // from docs: When write function is called with empty string or buffer, it does nothing and waits for more input.
+  // However, actually implementation checks the state of finished and aborted before checking if the first arg is empty.
   handleWrite(buffer, encoding, callback) {
-    debug('write', arguments)
+    debug('request write')
     const { req } = this
 
-    if (!req.aborted) {
-      if (buffer) {
-        if (!Buffer.isBuffer(buffer)) {
-          buffer = Buffer.from(buffer, encoding)
-        }
-        this.requestBodyBuffers.push(buffer)
-      }
-      // can't use instanceof Function because some test runners
-      // run tests in vm.runInNewContext where Function is not same
-      // as that in the current context
-      // https://github.com/nock/nock/pull/1754#issuecomment-571531407
-      if (typeof callback === 'function') {
-        callback()
-      }
-    } else {
-      this.emitError(new Error('Request aborted'))
+    if (req.finished) {
+      const err = new Error('write after end')
+      err.code = 'ERR_STREAM_WRITE_AFTER_END'
+      process.nextTick(() => req.emit('error', err))
+
+      // It seems odd to return `true` here, not sure why you'd want to have
+      // the stream potentially written to more, but it's what Node does.
+      // https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js#L662-L665
+      return true
+    }
+
+    if (req.socket && req.socket.destroyed) {
+      return false
+    }
+
+    if (!buffer || buffer.length === 0) {
+      return true
+    }
+
+    if (!Buffer.isBuffer(buffer)) {
+      buffer = Buffer.from(buffer, encoding)
+    }
+    this.requestBodyBuffers.push(buffer)
+
+    // can't use instanceof Function because some test runners
+    // run tests in vm.runInNewContext where Function is not same
+    // as that in the current context
+    // https://github.com/nock/nock/pull/1754#issuecomment-571531407
+    if (typeof callback === 'function') {
+      callback()
     }
 
     common.setImmediate(function () {
@@ -139,10 +179,10 @@ class InterceptedRequestRouter {
   }
 
   handleEnd(chunk, encoding, callback) {
-    debug('req.end')
+    debug('request end')
     const { req } = this
 
-    // handle the different overloaded param signatures
+    // handle the different overloaded arg signatures
     if (typeof chunk === 'function') {
       callback = chunk
       chunk = null
@@ -155,51 +195,16 @@ class InterceptedRequestRouter {
       req.once('finish', callback)
     }
 
-    if (!req.aborted && !this.playbackStarted) {
-      req.write(chunk, encoding)
-      this.startPlayback()
-    }
-    if (req.aborted) {
-      this.emitError(new Error('Request aborted'))
-    }
+    req.write(chunk, encoding)
+    req.finished = true
+    this.maybeStartPlayback()
 
     return req
   }
 
   handleFlushHeaders() {
-    debug('req.flushHeaders')
-    const { req } = this
-
-    if (!req.aborted && !this.playbackStarted) {
-      this.startPlayback()
-    }
-    if (req.aborted) {
-      this.emitError(new Error('Request aborted'))
-    }
-  }
-
-  handleAbort() {
-    debug('req.abort')
-    const { req, response, socket } = this
-
-    if (req.aborted) {
-      return
-    }
-    req.aborted = Date.now()
-    if (!this.playbackStarted) {
-      this.startPlayback()
-    }
-    const err = new Error()
-    err.code = 'aborted'
-    response.emit('close', err)
-
-    socket.destroy()
-
-    req.emit('abort')
-
-    const connResetError = new Error('socket hang up')
-    connResetError.code = 'ECONNRESET'
-    this.emitError(connResetError)
+    debug('request flushHeaders')
+    this.maybeStartPlayback()
   }
 
   /**
@@ -233,6 +238,22 @@ class InterceptedRequestRouter {
     }
   }
 
+  maybeStartPlayback() {
+    const { req, socket, playbackStarted } = this
+
+    // In order to get the events in the right order we need to delay playback
+    // if we get here before the `socket` event is emitted.
+    if (socket.connecting) {
+      this.readyToStartPlaybackOnSocketEvent = true
+      return
+    }
+
+    // Until Node 14 is the minimum, we need to look at both flags to see if the request has been cancelled.
+    if (!req.destroyed && !req.aborted && !playbackStarted) {
+      this.startPlayback()
+    }
+  }
+
   startPlayback() {
     debug('ending')
     this.playbackStarted = true
@@ -270,11 +291,12 @@ class InterceptedRequestRouter {
     )
 
     if (matchedInterceptor) {
-      debug('interceptor identified, starting mocking')
+      matchedInterceptor.scope.logger(
+        'interceptor identified, starting mocking'
+      )
 
       // wait to emit the finish event until we know for sure an Interceptor is going to playback.
       // otherwise an unmocked request might emit finish twice.
-      req.finished = true
       req.emit('finish')
 
       playbackInterceptor({
@@ -304,14 +326,11 @@ class InterceptedRequestRouter {
         // We send the raw buffer as we received it, not as we interpreted it.
         newReq.end(requestBodyBuffer)
       } else {
-        const err = new Error(
-          `Nock: No match for request ${common.stringifyRequest(
-            options,
-            requestBodyString
-          )}`
-        )
+        const reqStr = common.stringifyRequest(options, requestBodyString)
+        const err = new Error(`Nock: No match for request ${reqStr}`)
+        err.code = 'ERR_NOCK_NO_MATCH'
         err.statusCode = err.status = 404
-        this.emitError(err)
+        req.destroy(err)
       }
     }
   }
diff --git a/lib/interceptor.js b/lib/interceptor.js
index 8d1cccb..0a76bba 100644
--- a/lib/interceptor.js
+++ b/lib/interceptor.js
@@ -1,8 +1,6 @@
 'use strict'
 
-const debug = require('debug')('nock.interceptor')
 const stringify = require('json-stringify-safe')
-const _ = require('lodash')
 const querystring = require('querystring')
 const { URL, URLSearchParams } = require('url')
 
@@ -73,7 +71,7 @@ module.exports = class Interceptor {
       scope.scopeOptions.badheaders || []
     )
 
-    this.delayInMs = 0
+    this.delayBodyInMs = 0
     this.delayConnectionInMs = 0
 
     this.optional = false
@@ -181,8 +179,8 @@ module.exports = class Interceptor {
       }
     }
 
-    debug('reply.headers:', this.headers)
-    debug('reply.rawHeaders:', this.rawHeaders)
+    this.scope.logger('reply.headers:', this.headers)
+    this.scope.logger('reply.rawHeaders:', this.rawHeaders)
 
     this.body = body
 
@@ -228,13 +226,23 @@ module.exports = class Interceptor {
       }
     }
 
-    debug("request header field doesn't match:", key, header, reqHeader)
+    this.scope.logger(
+      "request header field doesn't match:",
+      key,
+      header,
+      reqHeader
+    )
     return false
   }
 
   match(req, options, body) {
-    if (debug.enabled) {
-      debug('match %s, body = %s', stringify(options), stringify(body))
+    // check if the logger is enabled because the stringifies can be expensive.
+    if (this.scope.logger.enabled) {
+      this.scope.logger(
+        'attempting match %s, body = %s',
+        stringify(options),
+        stringify(body)
+      )
     }
 
     const method = (options.method || 'GET').toUpperCase()
@@ -244,7 +252,7 @@ module.exports = class Interceptor {
     const { proto } = options
 
     if (this.method !== method) {
-      debug(
+      this.scope.logger(
         `Method did not match. Request ${method} Interceptor ${this.method}`
       )
       return false
@@ -276,6 +284,7 @@ module.exports = class Interceptor {
     )
 
     if (!reqHeadersMatch) {
+      this.scope.logger("headers don't match")
       return false
     }
 
@@ -283,26 +292,32 @@ module.exports = class Interceptor {
       this.scope.scopeOptions.conditionally &&
       !this.scope.scopeOptions.conditionally()
     ) {
+      this.scope.logger(
+        'matching failed because Scope.conditionally() did not validate'
+      )
       return false
     }
 
-    const reqContainsBadHeaders = this.badheaders.some(
+    const badHeaders = this.badheaders.filter(
       header => header in options.headers
     )
 
-    if (reqContainsBadHeaders) {
+    if (badHeaders.length) {
+      this.scope.logger('request contains bad headers', ...badHeaders)
       return false
     }
 
     // Match query strings when using query()
     if (this.queries === null) {
-      debug('query matching skipped')
+      this.scope.logger('query matching skipped')
     } else {
       // can't rely on pathname or search being in the options, but path has a default
       const [pathname, search] = path.split('?')
       const matchQueries = this.matchQuery({ search })
 
-      debug(matchQueries ? 'query matching succeeded' : 'query matching failed')
+      this.scope.logger(
+        matchQueries ? 'query matching succeeded' : 'query matching failed'
+      )
 
       if (!matchQueries) {
         return false
@@ -405,8 +420,8 @@ module.exports = class Interceptor {
     }
 
     const reqQueries = querystring.parse(options.search)
-    debug('Interceptor queries: %j', this.queries)
-    debug('    Request queries: %j', reqQueries)
+    this.scope.logger('Interceptor queries: %j', this.queries)
+    this.scope.logger('    Request queries: %j', reqQueries)
 
     if (typeof this.queries === 'function') {
       return this.queries(reqQueries)
@@ -483,7 +498,7 @@ module.exports = class Interceptor {
       // Normalize the data into the shape that is matched against.
       // Duplicate keys are handled by combining the values into an array.
       queries = querystring.parse(queries.toString())
-    } else if (!_.isPlainObject(queries)) {
+    } else if (!common.isPlainObject(queries)) {
       throw Error(`Argument Error: ${queries}`)
     }
 
@@ -583,7 +598,7 @@ module.exports = class Interceptor {
    * @return {Interceptor} - the current interceptor for chaining
    */
   delayBody(ms) {
-    this.delayInMs += ms
+    this.delayBodyInMs = ms
     return this
   }
 
@@ -594,26 +609,7 @@ module.exports = class Interceptor {
    * @return {Interceptor} - the current interceptor for chaining
    */
   delayConnection(ms) {
-    this.delayConnectionInMs += ms
-    return this
-  }
-
-  /**
-   * @private
-   * @returns {number}
-   */
-  getTotalDelay() {
-    return this.delayInMs + this.delayConnectionInMs
-  }
-
-  /**
-   * Make the socket idle for a certain number of ms (simulated).
-   *
-   * @param  {integer} ms - Number of milliseconds to wait
-   * @return {Interceptor} - the current interceptor for chaining
-   */
-  socketDelay(ms) {
-    this.socketDelayInMs = ms
+    this.delayConnectionInMs = ms
     return this
   }
 }
diff --git a/lib/match_body.js b/lib/match_body.js
index 6eda62d..73f753a 100644
--- a/lib/match_body.js
+++ b/lib/match_body.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const _ = require('lodash')
 const querystring = require('querystring')
 
 const common = require('./common')
@@ -70,8 +69,8 @@ function mapValuesDeep(obj, cb) {
   if (Array.isArray(obj)) {
     return obj.map(v => mapValuesDeep(v, cb))
   }
-  if (_.isPlainObject(obj)) {
-    return _.mapValues(obj, v => mapValuesDeep(v, cb))
+  if (common.isPlainObject(obj)) {
+    return common.mapValue(obj, v => mapValuesDeep(v, cb))
   }
   return cb(obj)
 }
diff --git a/lib/playback_interceptor.js b/lib/playback_interceptor.js
index ffe4d05..56fc0bd 100644
--- a/lib/playback_interceptor.js
+++ b/lib/playback_interceptor.js
@@ -1,10 +1,10 @@
 'use strict'
 
+const stream = require('stream')
 const util = require('util')
 const zlib = require('zlib')
 const debug = require('debug')('nock.playback_interceptor')
 const common = require('./common')
-const DelayedBody = require('./delayed_body')
 
 function parseJSONRequestBody(req, requestBody) {
   if (!requestBody || !common.isJSONContent(req.headers)) {
@@ -71,6 +71,44 @@ function selectDefaultHeaders(existingHeaders, defaultHeaders) {
   return result
 }
 
+// Presents a list of Buffers as a Readable
+class ReadableBuffers extends stream.Readable {
+  constructor(buffers, opts = {}) {
+    super(opts)
+
+    this.buffers = buffers
+  }
+
+  _read(size) {
+    while (this.buffers.length) {
+      if (!this.push(this.buffers.shift())) {
+        return
+      }
+    }
+    this.push(null)
+  }
+}
+
+function convertBodyToStream(body) {
+  if (common.isStream(body)) {
+    return body
+  }
+
+  if (body === undefined) {
+    return new ReadableBuffers([])
+  }
+
+  if (Buffer.isBuffer(body)) {
+    return new ReadableBuffers([body])
+  }
+
+  if (typeof body !== 'string') {
+    body = JSON.stringify(body)
+  }
+
+  return new ReadableBuffers([Buffer.from(body)])
+}
+
 /**
  * Play back an interceptor using the given request and mock response.
  */
@@ -83,28 +121,25 @@ function playbackInterceptor({
   response,
   interceptor,
 }) {
-  function emitError(error) {
-    process.nextTick(() => {
-      req.emit('error', error)
-    })
-  }
+  const { logger } = interceptor.scope
 
   function start() {
+    interceptor.markConsumed()
     interceptor.req = req
     req.headers = req.getHeaders()
 
     interceptor.scope.emit('request', req, interceptor, requestBodyString)
 
     if (typeof interceptor.errorMessage !== 'undefined') {
-      interceptor.markConsumed()
-
       let error
       if (typeof interceptor.errorMessage === 'object') {
         error = interceptor.errorMessage
       } else {
         error = new Error(interceptor.errorMessage)
       }
-      common.setTimeout(() => emitError(error), interceptor.getTotalDelay())
+
+      const delay = interceptor.delayBodyInMs + interceptor.delayConnectionInMs
+      common.setTimeout(() => req.destroy(error), delay)
       return
     }
 
@@ -114,7 +149,7 @@ function playbackInterceptor({
 
     // Clone headers/rawHeaders to not override them when evaluating later
     response.rawHeaders = [...interceptor.rawHeaders]
-    debug('response.rawHeaders:', response.rawHeaders)
+    logger('response.rawHeaders:', response.rawHeaders)
 
     if (interceptor.replyFunction) {
       const parsedRequestBody = parseJSONRequestBody(req, requestBodyString)
@@ -128,8 +163,8 @@ function playbackInterceptor({
       // At this point `fn` is either a synchronous function or a promise-returning function;
       // wrapping in `Promise.resolve` makes it into a promise either way.
       Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
-        .then(responseBody => continueWithResponseBody({ responseBody }))
-        .catch(err => emitError(err))
+        .then(continueWithResponseBody)
+        .catch(err => req.destroy(err))
       return
     }
 
@@ -142,8 +177,8 @@ function playbackInterceptor({
       }
 
       Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
-        .then(fullReplyResult => continueWithFullResponse({ fullReplyResult }))
-        .catch(err => emitError(err))
+        .then(continueWithFullResponse)
+        .catch(err => req.destroy(err))
       return
     }
 
@@ -155,21 +190,12 @@ function playbackInterceptor({
       //  of response buffers which should be mocked one by one.
       //  (otherwise decompressions after the first one fails as unzip expects to receive
       //  buffer by buffer and not one single merged buffer)
-
-      if (interceptor.delayInMs) {
-        emitError(
-          new Error(
-            'Response delay of the body is currently not supported with content-encoded responses.'
-          )
-        )
-        return
-      }
-
       const bufferData = Array.isArray(interceptor.body)
         ? interceptor.body
         : [interceptor.body]
       const responseBuffers = bufferData.map(data => Buffer.from(data, 'hex'))
-      continueWithResponseBody({ responseBuffers })
+      const responseBody = new ReadableBuffers(responseBuffers)
+      continueWithResponseBody(responseBody)
       return
     }
 
@@ -194,155 +220,103 @@ function playbackInterceptor({
       }
     }
 
-    return continueWithResponseBody({ responseBody })
+    return continueWithResponseBody(responseBody)
   }
 
-  function continueWithFullResponse({ fullReplyResult }) {
+  function continueWithFullResponse(fullReplyResult) {
     let responseBody
     try {
       responseBody = parseFullReplyResult(response, fullReplyResult)
-    } catch (innerErr) {
-      emitError(innerErr)
+    } catch (err) {
+      req.destroy(err)
       return
     }
 
-    continueWithResponseBody({ responseBody })
+    continueWithResponseBody(responseBody)
   }
 
-  function continueWithResponseBody({ responseBuffers, responseBody }) {
-    //  Transform the response body if it exists (it may not exist
-    //  if we have `responseBuffers` instead)
-    if (responseBody !== undefined) {
-      debug('transform the response body')
-
-      if (interceptor.delayInMs) {
-        debug(
-          'delaying the response for',
-          interceptor.delayInMs,
-          'milliseconds'
-        )
-        // Because setTimeout is called immediately in DelayedBody(), so we
-        // need count in the delayConnectionInMs.
-        responseBody = new DelayedBody(
-          interceptor.getTotalDelay(),
-          responseBody
-        )
-      }
+  function prepareResponseHeaders(body) {
+    const defaultHeaders = [...interceptor.scope._defaultReplyHeaders]
 
-      if (common.isStream(responseBody)) {
-        debug('response body is a stream')
-        responseBody.pause()
-        responseBody.on('data', function (d) {
-          response.push(d)
-        })
-        responseBody.on('end', function () {
-          response.push(null)
-          // https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_message_complete
-          response.complete = true
-        })
-        responseBody.on('error', function (err) {
-          response.emit('error', err)
-        })
-      } else if (!Buffer.isBuffer(responseBody)) {
-        if (typeof responseBody === 'string') {
-          responseBody = Buffer.from(responseBody)
-        } else {
-          responseBody = JSON.stringify(responseBody)
-          response.rawHeaders.push('Content-Type', 'application/json')
-        }
-      }
-      // Why are strings converted to a Buffer, but JSON data is left as a string?
-      // Related to https://github.com/nock/nock/issues/1542 ?
-    }
+    // Include a JSON content type when JSON.stringify is called on the body.
+    // This is a convenience added by Nock that has no analog in Node. It's added to the
+    // defaults, so it will be ignored if the caller explicitly provided the header already.
+    const isJSON =
+      body !== undefined &&
+      typeof body !== 'string' &&
+      !Buffer.isBuffer(body) &&
+      !common.isStream(body)
 
-    interceptor.markConsumed()
-
-    if (req.aborted) {
-      return
+    if (isJSON) {
+      defaultHeaders.push('Content-Type', 'application/json')
     }
 
     response.rawHeaders.push(
-      ...selectDefaultHeaders(
-        response.rawHeaders,
-        interceptor.scope._defaultReplyHeaders
-      )
+      ...selectDefaultHeaders(response.rawHeaders, defaultHeaders)
     )
 
     // Evaluate functional headers.
     common.forEachHeader(response.rawHeaders, (value, fieldName, i) => {
       if (typeof value === 'function') {
-        response.rawHeaders[i + 1] = value(req, response, responseBody)
+        response.rawHeaders[i + 1] = value(req, response, body)
       }
     })
 
     response.headers = common.headersArrayToObject(response.rawHeaders)
-
-    process.nextTick(() =>
-      respondUsingInterceptor({
-        responseBody,
-        responseBuffers,
-      })
-    )
   }
 
-  function respondUsingInterceptor({ responseBody, responseBuffers }) {
-    if (req.aborted) {
-      return
-    }
+  function continueWithResponseBody(rawBody) {
+    prepareResponseHeaders(rawBody)
+    const bodyAsStream = convertBodyToStream(rawBody)
+    bodyAsStream.pause()
+
+    // IncomingMessage extends Readable so we can't simply pipe.
+    bodyAsStream.on('data', function (chunk) {
+      response.push(chunk)
+    })
+    bodyAsStream.on('end', function () {
+      // https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_message_complete
+      response.complete = true
+      response.push(null)
+
+      interceptor.scope.emit('replied', req, interceptor)
+    })
+    bodyAsStream.on('error', function (err) {
+      response.emit('error', err)
+    })
+
+    const { delayBodyInMs, delayConnectionInMs } = interceptor
 
     function respond() {
       if (req.aborted) {
         return
       }
 
-      debug('emitting response')
-      req.emit('response', response)
+      // Even though we've had the response object for awhile at this point,
+      // we only attach it to the request immediately before the `response`
+      // event because, as in Node, it alters the error handling around aborts.
+      req.res = response
+      response.req = req
 
-      if (common.isStream(responseBody)) {
-        debug('resuming response stream')
-        responseBody.resume()
-      } else {
-        responseBuffers = responseBuffers || []
-        if (typeof responseBody !== 'undefined') {
-          debug('adding body to buffer list')
-          responseBuffers.push(responseBody)
-        }
-
-        // Stream the response chunks one at a time.
-        common.setImmediate(function emitChunk() {
-          const chunk = responseBuffers.shift()
-
-          if (chunk) {
-            debug('emitting response chunk')
-            response.push(chunk)
-            common.setImmediate(emitChunk)
-          } else {
-            debug('ending response stream')
-            response.push(null)
-            // https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_message_complete
-            response.complete = true
-            interceptor.scope.emit('replied', req, interceptor)
-          }
-        })
-      }
-    }
+      logger('emitting response')
+      req.emit('response', response)
 
-    if (interceptor.socketDelayInMs && interceptor.socketDelayInMs > 0) {
-      socket.applyDelay(interceptor.socketDelayInMs)
+      common.setTimeout(() => bodyAsStream.resume(), delayBodyInMs)
     }
 
-    if (
-      interceptor.delayConnectionInMs &&
-      interceptor.delayConnectionInMs > 0
-    ) {
-      socket.applyDelay(interceptor.delayConnectionInMs)
-      common.setTimeout(respond, interceptor.delayConnectionInMs)
-    } else {
-      respond()
-    }
+    socket.applyDelay(delayConnectionInMs)
+    common.setTimeout(respond, delayConnectionInMs)
   }
 
-  start()
+  // Calling `start` immediately could take the request all the way to the connection delay
+  // during a single microtask execution. This setImmediate stalls the playback to ensure the
+  // correct events are emitted first ('socket', 'finish') and any aborts in the in the queue or
+  // called during a 'finish' listener can be called.
+  common.setImmediate(() => {
+    if (!req.aborted) {
+      start()
+    }
+  })
 }
 
 module.exports = { playbackInterceptor }
diff --git a/lib/scope.js b/lib/scope.js
index 73875d7..d1fb314 100644
--- a/lib/scope.js
+++ b/lib/scope.js
@@ -9,7 +9,6 @@ const assert = require('assert')
 const url = require('url')
 const debug = require('debug')('nock.scope')
 const { EventEmitter } = require('events')
-const util = require('util')
 const Interceptor = require('./interceptor')
 
 let fs
@@ -40,7 +39,6 @@ class Scope extends EventEmitter {
     this.transformPathFunction = null
     this.transformRequestBodyFunction = null
     this.matchHeaders = []
-    this.logger = debug
     this.scopeOptions = options || {}
     this.urlParts = {}
     this._persist = false
@@ -51,13 +49,18 @@ class Scope extends EventEmitter {
     this.port = null
     this._defaultReplyHeaders = []
 
+    let logNamespace = String(basePath)
+
     if (!(basePath instanceof RegExp)) {
       this.urlParts = url.parse(basePath)
       this.port =
         this.urlParts.port || (this.urlParts.protocol === 'http:' ? 80 : 443)
       this.basePathname = this.urlParts.pathname.replace(/\/$/, '')
       this.basePath = `${this.urlParts.protocol}//${this.urlParts.hostname}:${this.port}`
+      logNamespace = this.urlParts.host
     }
+
+    this.logger = debug.extend(logNamespace)
   }
 
   add(key, interceptor) {
@@ -219,11 +222,6 @@ class Scope extends EventEmitter {
     return this
   }
 
-  log(newLogger) {
-    this.logger = newLogger
-    return this
-  }
-
   persist(flag = true) {
     if (typeof flag !== 'boolean') {
       throw new Error('Invalid arguments: argument should be a boolean')
@@ -306,13 +304,6 @@ function tryJsonParse(string) {
   }
 }
 
-// Use a noop deprecate util instead calling emitWarning directly so we get --no-deprecation and single warning behavior for free.
-const emitAsteriskDeprecation = util.deprecate(
-  () => {},
-  'Skipping body matching using "*" is deprecated. Set the definition body to undefined instead.',
-  'NOCK1579'
-)
-
 function define(nockDefs) {
   const scopes = []
 
@@ -335,17 +326,6 @@ function define(nockDefs) {
     options.reqheaders = reqheaders
     options.badheaders = badheaders
 
-    let { body } = nockDef
-
-    if (body === '*') {
-      // In previous versions, it was impossible to NOT filter on request bodies. This special value
-      // is sniffed out for users manipulating the definitions and not wanting to match on the
-      // request body. For newer versions, users should remove the `body` key or set to `undefined`
-      // to achieve the same affect. Maintaining legacy behavior for now.
-      emitAsteriskDeprecation()
-      body = undefined
-    }
-
     // Response is not always JSON as it could be a string or binary data or
     // even an array of binary buffers (e.g. when content is encoded).
     let response
@@ -375,7 +355,9 @@ function define(nockDefs) {
       }
     })
 
-    scope.intercept(npath, method, body).reply(status, response, rawHeaders)
+    scope
+      .intercept(npath, method, nockDef.body)
+      .reply(status, response, rawHeaders)
 
     scopes.push(scope)
   })
diff --git a/lib/socket.js b/lib/socket.js
index 02f0cb7..2777ae2 100644
--- a/lib/socket.js
+++ b/lib/socket.js
@@ -18,14 +18,13 @@ module.exports = class Socket extends EventEmitter {
     this.readable = true
     this.pending = false
     this.destroyed = false
-    this.connecting = false
+    this.connecting = true
 
-    // totalDelay that has already been applied to the current
-    // request/connection, timeout error will be generated if
-    // it is timed-out.
-    this.totalDelayMs = 0
-    // Maximum allowed delay. Null means unlimited.
-    this.timeoutMs = null
+    // Undocumented flag used by ClientRequest to ensure errors aren't double-fired
+    this._hadError = false
+
+    // Maximum allowed delay. 0 means unlimited.
+    this.timeout = 0
 
     const ipv6 = options.family === 6
     this.remoteFamily = ipv6 ? 'IPv6' : 'IPv4'
@@ -48,16 +47,22 @@ module.exports = class Socket extends EventEmitter {
   }
 
   setTimeout(timeoutMs, fn) {
-    this.timeoutMs = timeoutMs
+    this.timeout = timeoutMs
     if (fn) {
       this.once('timeout', fn)
     }
+    return this
   }
 
+  /**
+   * Artificial delay that will trip socket timeouts when appropriate.
+   *
+   * Doesn't actually wait for time to pass.
+   * Timeout events don't necessarily end the request.
+   * While many clients choose to abort the request upon a timeout, Node itself does not.
+   */
   applyDelay(delayMs) {
-    this.totalDelayMs += delayMs
-
-    if (this.timeoutMs && this.totalDelayMs > this.timeoutMs) {
+    if (this.timeout && delayMs > this.timeout) {
       debug('socket timeout')
       this.emit('timeout')
     }
@@ -69,12 +74,30 @@ module.exports = class Socket extends EventEmitter {
     ).toString('base64')
   }
 
+  /**
+   * Denotes that no more I/O activity should happen on this socket.
+   *
+   * The implementation in Node if far more complex as it juggles underlying async streams.
+   * For the purposes of Nock, we just need it to set some flags and on the first call
+   * emit a 'close' and optional 'error' event. Both events propagate through the request object.
+   */
   destroy(err) {
+    if (this.destroyed) {
+      return this
+    }
+
+    debug('socket destroy')
     this.destroyed = true
     this.readable = this.writable = false
-    if (err) {
-      this.emit('error', err)
-    }
+
+    process.nextTick(() => {
+      if (err) {
+        this._hadError = true
+        this.emit('error', err)
+      }
+      this.emit('close')
+    })
+
     return this
   }
 }
diff --git a/migration_guides/migrating_to_10.md b/migration_guides/migrating_to_10.md
new file mode 100644
index 0000000..f5491bb
--- /dev/null
+++ b/migration_guides/migrating_to_10.md
@@ -0,0 +1,10 @@
+## Upgrading from Nock 9 to Nock 10
+
+[Release Tag](https://github.com/nock/nock/releases/tag/v10.0.0)
+
+### Breaking changes
+
+Support for Node < 6 was dropped.
+
+To upgrade Nock, ensure your version of Node is also updated.  
+At the time of release, Node 6.x, 8.x, and 9.x were supported.
diff --git a/migration_guides/migrating_to_11.md b/migration_guides/migrating_to_11.md
new file mode 100644
index 0000000..d111f85
--- /dev/null
+++ b/migration_guides/migrating_to_11.md
@@ -0,0 +1,212 @@
+## Upgrading from Nock 10 to Nock 11
+
+[Release Tag](https://github.com/nock/nock/releases/tag/v11.3.2)
+
+### Bug fixes and internal improvements
+
+Nock 11 includes many under-the-hood improvements, including a fully offline
+test suite and 100% test coverage. The codebase was also converted to ES6
+syntax and formatted with Prettier. Leaning on the test coverage, some
+substantial refactors have begun.
+
+Many bug fixes are included. See the detailed changelog below or the
+[compare view][compare] for details.
+
+### Fabulous new features for developers
+
+1. The library ships with TypeScript definitions. (Added in v11.3)
+1. Add support for the `http.request` signatures added in Node 10.9
+1. Scopes can be filtered using the system environment or any external factor
+   using e.g. `.conditionally(() => true)`
+1. In-flight modifications to headers are preserved in mock requests.
+1. Recorded mocks can be stringified using custom code in the `afterRecord()`
+   post-processing hook. When `afterRecord()` returns a string, the
+   recorder will no longer attempt to re-stringify it. (Added in v11.3)
+1. Reply functions passed to `.reply()` can now be async/promise-returning.
+1. Specifying reply headers, either via `.reply()` or `.defaultReplyHeaders()`,
+   can now be done consistently using an object, Map, or flat array.
+
+### Breaking changes
+
+For many developers no code changes will be needed. However, there are several
+minor changes to the API, and it's possible that you will need to update your
+code for Nock to keep working properly. It's unlikely that your tests will
+falsely pass; what's more probable is that your tests will fail until the
+necessary changes are made.
+
+1. Nock 11 requires Node 8 or later. Nock supports and tests all the "current"
+   and "maintenance" versions of Node. As of release, that's Node 8, 10, and 12.
+
+1. In Nock 10, when `reply()` was invoked with a function, the return values were
+   handled ambiguously depending on their types.
+
+   Consider the following example:
+
+   ```js
+   const scope = nock('http://example.com')
+     .get('/')
+     .reply(200, () => [500, 'hello world'])
+   ```
+
+   In Nock 10, the 200 was ignored, the 500 was interpreted as the status
+   code, and the body would contain `'hello world'`. This caused problems
+   when the goal was to return a numeric array, so in Nock 11, the 200 is
+   properly interpreted as the status code, and `[500, 'hello world']` as the
+   body.
+
+   These are the correct calls for Nock 11:
+
+   ```js
+   const scope = nock('http://example.com').get('/').reply(500, 'hello world')
+
+   const scope = nock('http://example.com')
+     .get('/')
+     .reply(500, () => 'hello world')
+   ```
+
+   The `.reply()` method can be called with explicit arguments:
+
+   ```js
+   .reply() // `statusCode` defaults to `200`.
+   .reply(statusCode) // `responseBody` defaults to `''`.
+   .reply(statusCode, responseBody) // `headers` defaults to `{}`.
+   .reply(statusCode, responseBody, headers)
+   ```
+
+   It can be called with a status code and a function that returns an array:
+
+   ```js
+   .reply(statusCode, (path, requestBody) => responseBody)
+   .reply(statusCode, (path, requestBody) => responseBody, headers)
+   ```
+
+   Alternatively the status code can be included in the array:
+
+   ```js
+   .reply((path, requestBody) => [statusCode])
+   .reply((path, requestBody) => [statusCode, responseBody])
+   .reply((path, requestBody) => [statusCode, responseBody, headers])
+   .reply((path, requestBody) => [statusCode, responseBody], headers)
+   ```
+
+   `.reply()` can also be called with an `async` or promise-returning function. The
+   signatures are identical, e.g.
+
+   ```js
+   .reply(async (path, requestBody) => [statusCode, responseBody])
+   .reply(statusCode, async (path, requestBody) => responseBody)
+   ```
+
+   Finally, an error-first callback can be used, e.g.:
+
+   ```js
+   .reply((path, requestBody, cb) => cb(undefined, [statusCode, responseBody]))
+   .reply(statusCode, (path, requestBody, cb) => cb(undefined, responseBody))
+   ```
+
+1. In Nock 10, errors in user-provided reply functions were caught by Nock, and
+   generated HTTP responses with status codes of 500. In Nock 11 these errors
+   are not caught, and instead are re-emitted through the request, like any
+   other error that occurs during request processing.
+
+   Consider the following example:
+
+   ```js
+   const scope = nock('http://example.com')
+     .post('/echo')
+     .reply(201, (uri, requestBody, cb) => {
+       fs.readFile('cat-poems.txt', cb) // Error-first callback
+     })
+   ```
+
+   When `fs.readFile()` errors in Nock 10, a 500 error was emitted. To get the
+   same effect in Nock 11, the example would need to be rewritten to:
+
+   ```js
+   const scope = nock('http://example.com')
+     .post('/echo')
+     .reply((uri, requestBody, cb) => {
+       fs.readFile('cat-poems.txt', (err, contents) => {
+         if (err) {
+           cb([500, err.stack])
+         } else {
+           cb([201, contents])
+         }
+       })
+     })
+   ```
+
+1. When `.reply()` is invoked with something other than a whole number status
+   code or a function, Nock 11 raises a new error **Invalid ... value for status code**.
+
+1. Callback functions provided to the `.query` method now receive the result of [`querystring.parse`](https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options) instead of [`qs.parse`](https://github.com/ljharb/qs#parsing-objects).
+
+   In particular, `querystring.parse` does not interpret keys with JSON
+   path notation:
+
+   ```js
+   querystring.parse('foo[bar]=baz') // { "foo[bar]": 'baz' }
+   qs.parse('foo[bar]=baz') // { foo: { bar: 'baz' } }
+   ```
+
+1. In Nock 10, duplicate field names provided to the `.query()` method were
+   silently ignored. We decided this was probably hiding unintentionally bugs
+   and causing frustration with users. In Nock 11, attempts to provide query
+   params more than once will throw a new error
+   **Query parameters have aleady been defined**. This could happen by calling
+   `.query()` twice, or by calling `.query()` after specifying a literal query
+   string via the path.
+
+   ```js
+   nock('http://example.com')
+     .get('/path')
+     .query({ foo: 'bar' })
+     .query({ baz: 'qux' }) // <-- will throw
+     .reply()
+
+   nock('http://example.com')
+     .get('/path?foo=bar')
+     .query({ baz: 'qux' }) // <-- will throw
+     .reply()
+   ```
+
+1. Paths in Nock have always required a leading slash. e.g.
+
+   ```js
+   const scope = nock('http://example.com').get('/path').reply()
+   ```
+
+   In Nock 10, if the leading slash was missing the mock would never match. In
+   Nock 11, this raises an error.
+
+1. The `reqheaders` parameter should be provided as a plain object, e.g.
+   `nock('http://example.com', { reqheaders: { X-Foo: 'bar' }})`. When the
+   headers are specified incorrectly as e.g. `{ reqheaders: 1 }`, Nock 10 would
+   behave in unpredictable ways. In Nock 11, a new error
+   **Headers must be provided as an object** is thrown.
+
+   ```js
+   nock('http://example.com', { reqheaders: 1 }).get('/').reply()
+   ```
+
+1. In Nock 10, the `ClientRequest` instance wrapped the native `on` method
+   and aliased `once` to it. In Nock 11, this been removed and `request.once`
+   will correctly call registered listeners...once.
+
+1. In Nock 10, when the method was not specified in a call to `nock.define()`,
+   the method would default to `GET`. In Nock 11, this raises an error.
+
+1. In very old versions of nock, recordings may include a response status
+   code encoded as a string in the `reply` field. In Nock 10 these strings could
+   be non-numeric. In Nock 11 this raises an error.
+
+### Updates to the mock surface
+
+1. For parity with a real response, a mock request correctly supports all
+   the overrides to `request.end()`, including `request.end(cb)` in Node 12.
+1. For parity with a real response, errors from the `.destroy()` method
+   are propagated correctly. (Added in v11.3)
+1. For parity with a real response, the `.complete` property is set when
+   ending the response.
+1. For parity with a real Socket, the mock Socket has an `unref()` function
+   (which does nothing).
diff --git a/migration_guides/migrating_to_12.md b/migration_guides/migrating_to_12.md
new file mode 100644
index 0000000..ee5f3f5
--- /dev/null
+++ b/migration_guides/migrating_to_12.md
@@ -0,0 +1,33 @@
+## Upgrading from Nock 11 to Nock 12
+
+[Release Tag](https://github.com/nock/nock/releases/tag/v12.0.0)
+
+### Breaking changes
+
+1. Support for Node < 10 was dropped.  
+   To upgrade Nock, ensure your version of Node is also updated.  
+   At the time of release, Node 10.x, 12.x, and 13.x were supported.
+
+1. [`cleanAll()`](https://github.com/nock/nock#cleanall) no longer returns the global `nock` instance ([#1872](https://github.com/nock/nock/pull/1872)).
+
+   ```js
+   // before
+   nock.cleanAll().restore() // Uncaught TypeError: Cannot read property 'restore' of undefined
+
+   // upgraded
+   nock.cleanAll()
+   nock.restore()
+   ```
+
+1. Support was dropped for the String constructor ([#1873](https://github.com/nock/nock/pull/1873)).  
+   Only string primitive are supported. All strings passed to Nock for options should not use `new String` syntax.
+   [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#String_primitives_and_String_objects).
+
+### New features for developers
+
+1.  [`enableNetConnect()`](https://github.com/nock/nock#enabling-requests) now accepts a function.
+    ```js
+    nock.enableNetConnect(
+      host => host.includes('amazon.com') || host.includes('github.com')
+    )
+    ```
diff --git a/migration_guides/migrating_to_13.md b/migration_guides/migrating_to_13.md
new file mode 100644
index 0000000..bb4454b
--- /dev/null
+++ b/migration_guides/migrating_to_13.md
@@ -0,0 +1,68 @@
+## Upgrading from Nock 12 to Nock 13
+
+[Release Tag](https://github.com/nock/nock/releases/tag/v13.0.0)
+
+### Breaking changes
+
+1. `Scope.log` has been removed. Use the `debug` library when [debugging](https://github.com/nock/nock#debugging) failed matches.
+
+1. `socketDelay` has been removed. Use [`delayConnection`](https://github.com/nock/nock#delay-the-connection) instead.
+
+1. `delay`, `delayConnection`, and `delayBody` are now setters instead of additive.
+
+   ```js
+   nock('http://example.com')
+     .get('/')
+     .delay(1)
+     .delay({ head: 2, body: 3 })
+     .delayConnection(4)
+     .delayBody(5)
+     .delayBody(6)
+     .reply()
+   ```
+
+   Previously, the connection would have been delayed by 7 and the body delayed by 14.
+   Now, the connection will be delayed by 4 and the body delayed by 6.
+
+1. [When recording](https://github.com/nock/nock#recording), skipping body matching using `*` is no longer supported by `nock.define`.
+   Set the definition body to `undefined` instead.
+
+   ```js
+   nock.define([
+     {
+       scope: 'http://example.test',
+       method: 'POST',
+       path: '/',
+       body: '*', // remove this line or set to undefined
+       response: 'matched',
+     },
+   ])
+   ```
+
+1. `ClientRequest.abort()` has been updated to align with Node's native behavior.
+
+   - Nock use to always emit a 'socket hang up' error. Now it only emits the error if `abort` is called between the 'socket' and 'response' events.
+   - The emitted 'abort' event now happens on `nextTick`.
+   - The socket is now only destroyed if the 'socket' event has fired, and now emits a 'close' event on `nextTick` that propagates though the request object.
+   - `request.aborted` attribute is set to `true` instead of a timestamp. [Changed in Node v11.0](https://github.com/nodejs/node/pull/20230).
+   - Calling `write`, `end`, or `flushHeaders` on an aborted request no longer emits an error.
+     However, writing to a request that is already finished (ended) will emit a 'write after end' error.
+
+1. Playback of a mocked responses will now never happen until the 'socket' event is emitted.
+   The 'socket' event is still artificially set to emit on `nextTick` when a ClientRequest is created.  
+   This means in the following code the Scope will never be done because at least one tick needs
+   to happen before any matched Interceptor is considered consumed.
+   ```js
+   const scope = nock(...).get('/').reply()
+   const req = http.get(...)
+   scope.done()
+   ```
+   The correct way to verify such an action is to call [`scope.done`](https://github.com/nock/nock#expectations) after waiting for a 'response', 'timeout', or 'socket' event on the request.  
+   For example:
+   ```js
+   const scope = nock(...).get('/').reply()
+   const req = http.get(...)
+   req.on('response', () => {
+     scope.done()
+   })
+   ```
diff --git a/package-lock.json b/package-lock.json
index b2f14f0..8020da3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3935,7 +3935,8 @@
     "lodash": {
       "version": "4.17.15",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+      "dev": true
     },
     "lodash.capitalize": {
       "version": "4.2.1",
@@ -3981,9 +3982,8 @@
     },
     "lodash.set": {
       "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-      "dev": true
+      "resolved": "http://artifactory.shuttercorp.net/artifactory/api/npm/npm-composite/lodash.set/-/lodash.set-4.3.2.tgz",
+      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
     },
     "lodash.toarray": {
       "version": "4.4.0",
diff --git a/package.json b/package.json
index 5b9e776..a9b172d 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
   "dependencies": {
     "debug": "^4.1.0",
     "json-stringify-safe": "^5.0.1",
-    "lodash": "^4.17.13",
+    "lodash.set": "^4.3.2",
     "propagate": "^2.0.0"
   },
   "devDependencies": {
diff --git a/types/index.d.ts b/types/index.d.ts
index 5d93ebc..9a81a90 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -120,7 +120,6 @@ declare namespace nock {
     filteringRequestBody(regex: RegExp, replace: string): this
     filteringRequestBody(fn: (body: string) => string): this
 
-    log(out: (message: any, ...optionalParams: any[]) => void): this
     persist(flag?: boolean): this
     replyContentLength(): this
     replyDate(d?: Date): this
@@ -204,7 +203,6 @@ declare namespace nock {
     delay(opts: number | { head?: number; body?: number }): this
     delayBody(timeMs: number): this
     delayConnection(timeMs: number): this
-    socketDelay(timeMs: number): this
   }
 
   interface Options {


Test patch:
diff --git a/tests/setup.js b/tests/setup.js
index ade96fc..bfbac16 100644
--- a/tests/setup.js
+++ b/tests/setup.js
@@ -16,6 +16,7 @@ chai.use(sinonChai)
 
 afterEach(function () {
   nock.restore()
+  nock.abortPendingRequests()
   nock.cleanAll()
   nock.enableNetConnect()
   nock.emitter.removeAllListeners()
diff --git a/tests/test_abort.js b/tests/test_abort.js
index b2986ff..7844486 100644
--- a/tests/test_abort.js
+++ b/tests/test_abort.js
@@ -1,157 +1,213 @@
 'use strict'
 
-const nock = require('..')
 const { expect } = require('chai')
 const http = require('http')
 const sinon = require('sinon')
+const nock = require('..')
 
 require('./setup')
 
-it('`req.abort()` should cause "abort" and "error" to be emitted', done => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayConnection(500)
-    .reply()
-
-  const onAbort = sinon.spy()
-  const req = http
-    .get('http://example.test/')
-    .once('abort', onAbort)
-    .once('error', err => {
-      // Should trigger last
-      expect(err.code).to.equal('ECONNRESET')
-      expect(onAbort).to.have.been.calledOnce()
-      scope.done()
-      done()
-    })
-  process.nextTick(() => req.abort())
-})
+// These tests use `setTimeout` before verifying emitted events to ensure any
+// number of `nextTicks` or `setImmediate` can process first.
 
-it('abort is emitted before delay time', done => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayConnection(500)
-    .reply()
-
-  const start = Date.now()
-  const req = http
-    .get('http://example.test/')
-    .once('abort', () => {
-      const actual = Date.now() - start
-      expect(actual).to.be.below(250)
-      scope.done()
+// Node will emit a `prefinish` event after `socket`, but it's an internal,
+// undocumented event that Nock does not emulate.
+
+// The order of tests run sequentially through a ClientRequest's lifetime.
+// Starting the top by aborting requests early on then aborting later and later.
+describe('`ClientRequest.abort()`', () => {
+  it('Emits the expected event sequence when `write` is called on an aborted request', done => {
+    const scope = nock('http://example.test').get('/').reply()
+
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
+    req.abort()
+    req.write('foo')
+
+    setTimeout(() => {
+      expect(emitSpy).to.have.been.calledOnceWithExactly('abort')
+      expect(scope.isDone()).to.be.false()
       done()
-    })
-    .once('error', () => {}) // Don't care.
-  // Don't bother with the response
+    }, 10)
+  })
 
-  setTimeout(() => req.abort(), 10)
-})
+  it('Emits the expected event sequence when `end` is called on an aborted request', done => {
+    const scope = nock('http://example.test').get('/').reply()
 
-it('Aborting an aborted request should not emit an error', done => {
-  const scope = nock('http://example.test').get('/').reply()
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
+    req.abort()
+    req.end()
 
-  let errorCount = 0
-  const req = http.get('http://example.test/').on('error', () => {
-    errorCount++
-    if (errorCount < 3) {
-      // Abort 3 times at max, otherwise this would be an endless loop.
-      // https://github.com/nock/nock/issues/882
-      req.abort()
-    }
+    setTimeout(() => {
+      expect(emitSpy).to.have.been.calledOnceWithExactly('abort')
+      expect(scope.isDone()).to.be.false()
+      done()
+    }, 10)
   })
-  req.abort()
-
-  // Allow some time to fail.
-  setTimeout(() => {
-    expect(errorCount).to.equal(1, 'Only one error should be sent')
-    scope.done()
-    done()
-  }, 10)
-})
 
-it('Aborting a not-yet-ended request should end it', done => {
-  // Set up.
-  const scope = nock('http://example.test').post('/').reply()
+  it('Emits the expected event sequence when `flushHeaders` is called on an aborted request', done => {
+    const scope = nock('http://example.test').get('/').reply()
+
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
+    req.abort()
+    req.flushHeaders()
 
-  const req = http.request({
-    host: 'example.test',
-    method: 'post',
-    path: '/',
+    setTimeout(() => {
+      expect(emitSpy).to.have.been.calledOnceWithExactly('abort')
+      expect(scope.isDone()).to.be.false()
+      done()
+    }, 10)
   })
-  req.on('error', () => {})
 
-  // Act.
-  req.abort()
+  it('Emits the expected event sequence when aborted immediately after `end`', done => {
+    const scope = nock('http://example.test').get('/').reply()
 
-  // Assert.
-  scope.done()
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
+    req.end()
+    req.abort()
 
-  done()
-})
+    setTimeout(() => {
+      expect(emitSpy).to.have.been.calledOnceWithExactly('abort')
+      expect(scope.isDone()).to.be.false()
+      done()
+    }, 10)
+  })
 
-it('`req.write() on an aborted request should trigger the expected error', done => {
-  const scope = nock('http://example.test').get('/').reply()
+  it('Emits the expected event sequence when aborted inside a `socket` event listener', done => {
+    const scope = nock('http://example.test').get('/').reply()
 
-  const req = http.get('http://example.test/')
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
 
-  req.once('error', err => {
-    // This is the expected first error event emitted, triggered by
-    // `req.abort()`.
-    expect(err.code).to.equal('ECONNRESET')
+    req.on('socket', () => {
+      req.abort()
+    })
+    req.on('error', err => {
+      expect(err.message).to.equal('socket hang up')
+      expect(err.code).to.equal('ECONNRESET')
+    })
+    req.end()
 
-    req.once('error', err => {
-      // This is the abort error under test, triggered by `req.write()`
-      expect(err.message).to.equal('Request aborted')
-      scope.done()
+    setTimeout(() => {
+      const events = emitSpy.args.map(i => i[0])
+      expect(events).to.deep.equal(['socket', 'abort', 'error', 'close'])
+      expect(scope.isDone()).to.be.false()
       done()
-    })
+    }, 10)
   })
 
-  process.nextTick(() => req.abort())
-  process.nextTick(() => req.write('some nonsense'))
-})
-
-it('`req.end()` on an aborted request should trigger the expected error', done => {
-  const scope = nock('http://example.test').get('/').reply()
+  it('Emits the expected event sequence when aborted multiple times', done => {
+    const scope = nock('http://example.test').get('/').reply()
 
-  const req = http.get('http://example.test/')
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
 
-  req.once('error', err => {
-    // This is the expected first error event emitted, triggered by
-    // `req.abort()`.
-    expect(err.code).to.equal('ECONNRESET')
+    req.on('error', () => {}) // listen for error so it doesn't bubble
+    req.on('socket', () => {
+      req.abort()
+      req.abort()
+      req.abort()
+    })
+    req.end()
 
-    req.once('error', err => {
-      // This is the abort error under test, triggered by `req.end()`
-      expect(err.message).to.equal('Request aborted')
-      scope.done()
+    setTimeout(() => {
+      const events = emitSpy.args.map(i => i[0])
+      // important: `abort` and `error` events only fire once and the `close` event still fires at the end
+      expect(events).to.deep.equal(['socket', 'abort', 'error', 'close'])
+      expect(scope.isDone()).to.be.false()
       done()
-    })
+    }, 10)
   })
 
-  process.nextTick(() => req.abort())
-  process.nextTick(() => req.end())
-})
+  it('Emits the expected event sequence when aborted inside a `finish` event listener', done => {
+    const scope = nock('http://example.test').get('/').reply()
+
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
+
+    req.on('finish', () => {
+      req.abort()
+    })
+    req.on('error', err => {
+      expect(err.message).to.equal('socket hang up')
+      expect(err.code).to.equal('ECONNRESET')
+    })
+    req.end()
+
+    setTimeout(() => {
+      const events = emitSpy.args.map(i => i[0])
+      expect(events).to.deep.equal([
+        'socket',
+        'finish',
+        'abort',
+        'error',
+        'close',
+      ])
+      expect(scope.isDone()).to.be.false()
+      done()
+    }, 10)
+  })
 
-it('`req.flushHeaders()` on an aborted request should trigger the expected error', done => {
-  const scope = nock('http://example.test').get('/').reply()
+  // The Interceptor is considered consumed just prior to the `response` event on the request,
+  // all tests below assert the Scope is done.
 
-  const req = http.get('http://example.test/')
+  it('Emits the expected event sequence when aborted after a delay from the `finish` event', done => {
+    // use the delay functionality to create a window where the abort is called during the artificial connection wait.
+    const scope = nock('http://example.test').get('/').delay(100).reply()
 
-  req.once('error', err => {
-    // This is the expected first error event emitted, triggered by
-    // `req.abort()`.
-    expect(err.code).to.equal('ECONNRESET')
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
 
-    req.once('error', err => {
-      // This is the abort error under test, triggered by `req.flushHeaders()`
-      expect(err.message).to.equal('Request aborted')
+    req.on('finish', () => {
+      setTimeout(() => {
+        req.abort()
+      }, 10)
+    })
+    req.on('error', err => {
+      expect(err.message).to.equal('socket hang up')
+      expect(err.code).to.equal('ECONNRESET')
+    })
+    req.end()
+
+    setTimeout(() => {
+      const events = emitSpy.args.map(i => i[0])
+      expect(events).to.deep.equal([
+        'socket',
+        'finish',
+        'abort',
+        'error',
+        'close',
+      ])
       scope.done()
       done()
-    })
+    }, 200)
   })
 
-  process.nextTick(() => req.abort())
-  process.nextTick(() => req.flushHeaders())
+  it('Emits the expected event sequence when aborted inside a `response` event listener', done => {
+    const scope = nock('http://example.test').get('/').reply()
+
+    const req = http.request('http://example.test')
+    const emitSpy = sinon.spy(req, 'emit')
+
+    req.on('response', () => {
+      req.abort()
+    })
+    req.end()
+
+    setTimeout(() => {
+      const events = emitSpy.args.map(i => i[0])
+      expect(events).to.deep.equal([
+        'socket',
+        'finish',
+        'response',
+        'abort',
+        'close',
+      ])
+      scope.done()
+      done()
+    }, 10)
+  })
 })
diff --git a/tests/test_back.js b/tests/test_back.js
index d0752cd..9ea87e1 100644
--- a/tests/test_back.js
+++ b/tests/test_back.js
@@ -2,11 +2,15 @@
 
 const http = require('http')
 const fs = require('fs')
+const { expect } = require('chai')
 const path = require('path')
+const sinon = require('sinon')
 const { beforeEach, test } = require('tap')
 const proxyquire = require('proxyquire').preserveCache()
 const nock = require('..')
+const { back: nockBack } = nock
 
+require('./setup')
 require('./cleanup_after_each')()
 
 beforeEach(done => {
@@ -15,11 +19,8 @@ beforeEach(done => {
   done()
 })
 
-const nockBack = nock.back
-const exists = fs.existsSync
-
 function testNock(t) {
-  let dataCalled = false
+  const onData = sinon.spy()
 
   const scope = nock('http://www.example.test')
     .get('/')
@@ -33,16 +34,16 @@ function testNock(t) {
         port: 80,
       },
       res => {
-        t.equal(res.statusCode, 200)
+        expect(res.statusCode).to.equal(200)
         res.once('end', () => {
-          t.ok(dataCalled)
+          expect(onData).to.have.been.called()
           scope.done()
           t.end()
         })
         res.on('data', data => {
-          dataCalled = true
-          t.ok(data instanceof Buffer, 'data should be a buffer')
-          t.equal(data.toString(), 'Hello World!', 'response should match')
+          onData()
+          expect(data).to.be.an.instanceOf(Buffer)
+          expect(data.toString()).to.equal('Hello World!')
         })
       }
     )
@@ -53,11 +54,12 @@ function nockBackWithFixture(t, scopesLoaded) {
   const scopesLength = scopesLoaded ? 1 : 0
 
   nockBack('good_request.json', function (done) {
-    t.equal(this.scopes.length, scopesLength)
-    http.get('http://www.example.test/')
-    this.assertScopesFinished()
-    done()
-    t.end()
+    expect(this.scopes).to.have.length(scopesLength)
+    http.get('http://www.example.test/', () => {
+      this.assertScopesFinished()
+      done()
+      t.end()
+    })
   })
 }
 
@@ -67,11 +69,12 @@ function nockBackWithFixture(t, scopesLoaded) {
 // comment.
 function nockBackWithFixtureLocalhost(t) {
   nockBack('goodRequestLocalhost.json', function (done) {
-    t.equal(this.scopes.length, 0)
+    const onRequest = sinon.spy()
 
-    const server = http.createServer((request, response) => {
-      t.pass('server received a request')
+    expect(this.scopes).to.be.empty()
 
+    const server = http.createServer((request, response) => {
+      onRequest()
       response.writeHead(200)
       response.end()
     })
@@ -86,7 +89,8 @@ function nockBackWithFixtureLocalhost(t) {
           port: server.address().port,
         },
         response => {
-          t.is(200, response.statusCode)
+          expect(onRequest).to.have.been.calledOnce()
+          expect(response.statusCode).to.equal(200)
           this.assertScopesFinished()
           done()
           t.end()
@@ -102,45 +106,44 @@ function nockBackWithFixtureLocalhost(t) {
 test('nockBack throws an exception when fixtures is not set', t => {
   nockBack.fixtures = undefined
 
-  t.throws(nockBack, { message: 'Back requires nock.back.fixtures to be set' })
+  expect(nockBack).to.throw('Back requires nock.back.fixtures to be set')
   t.end()
 })
 
 test('nockBack throws an exception when fixtureName is not a string', t => {
-  t.throws(() => nockBack(), {
-    message: 'Parameter fixtureName must be a string',
-  })
+  expect(nockBack).to.throw('Parameter fixtureName must be a string')
   t.end()
 })
 
 test('nockBack returns a promise when neither options nor nockbackFn are specified', t => {
-  nockBack('test-promise-fixture.json').then(params => {
-    t.type(params.nockDone, 'function')
-    t.type(params.context, 'object')
+  nockBack('test-promise-fixture.json').then(({ nockDone, context }) => {
+    expect(nockDone).to.be.a('function')
+    expect(context).to.be.an('object')
     t.end()
   })
 })
 
 test('nockBack throws an exception when a hook is not a function', t => {
   nockBack.setMode('dryrun')
-  t.throws(
-    () => nockBack('good_request.json', { before: 'not-a-function-innit' }),
-    { message: 'processing hooks must be a function' }
-  )
+  expect(() =>
+    nockBack('good_request.json', { before: 'not-a-function-innit' })
+  ).to.throw('processing hooks must be a function')
   t.end()
 })
 
 test('nockBack.setMode throws an exception on unknown mode', t => {
-  t.throws(() => nockBack.setMode('bogus'), { message: 'Unknown mode: bogus' })
+  expect(() => nockBack.setMode('bogus')).to.throw('Unknown mode: bogus')
   t.end()
 })
 
 test('nockBack returns a promise when nockbackFn is not specified', t => {
-  nockBack('test-promise-fixture.json', { test: 'options' }).then(params => {
-    t.type(params.nockDone, 'function')
-    t.type(params.context, 'object')
-    t.end()
-  })
+  nockBack('test-promise-fixture.json', { test: 'options' }).then(
+    ({ nockDone, context }) => {
+      expect(nockDone).to.be.a('function')
+      expect(context).to.be.an('object')
+      t.end()
+    }
+  )
 })
 
 test('with wild, normal nocks work', t => testNock(t))
@@ -166,10 +169,10 @@ test('nockBack dryrun tests', nw => {
   })
 
   nw.test('goes to internet even when no nockBacks are running', t => {
-    t.plan(2)
+    const onRequest = sinon.spy()
 
     const server = http.createServer((request, response) => {
-      t.pass('server received a request')
+      onRequest()
 
       response.writeHead(200)
       response.end()
@@ -183,7 +186,8 @@ test('nockBack dryrun tests', nw => {
           port: server.address().port,
         },
         response => {
-          t.is(200, response.statusCode)
+          expect(response.statusCode).to.equal(200)
+          expect(onRequest).to.have.been.calledOnce()
 
           server.close(t.end)
         }
@@ -199,19 +203,17 @@ test('nockBack dryrun tests', nw => {
   nw.test('uses recorded fixtures', t => nockBackWithFixture(t, true))
 
   nw.test("goes to internet, doesn't record new fixtures", t => {
-    t.plan(5)
-
-    let dataCalled = false
+    const onData = sinon.spy()
+    const onRequest = sinon.spy()
 
     const fixture = 'someDryrunFixture.json'
     const fixtureLoc = `${nockBack.fixtures}/${fixture}`
 
-    t.false(exists(fixtureLoc))
+    expect(fs.existsSync(fixtureLoc)).to.be.false()
 
-    nockBack(fixture, done => {
+    nockBack(fixture, nockDone => {
       const server = http.createServer((request, response) => {
-        t.pass('server received a request')
-
+        onRequest()
         response.writeHead(200)
         response.write('server served a response')
         response.end()
@@ -225,15 +227,14 @@ test('nockBack dryrun tests', nw => {
             port: server.address().port,
           },
           response => {
-            t.is(200, response.statusCode)
+            expect(response.statusCode).to.equal(200)
 
-            response.on('data', data => {
-              dataCalled = true
-            })
+            response.on('data', onData)
 
             response.on('end', () => {
-              t.ok(dataCalled)
-              t.false(exists(fixtureLoc))
+              expect(onRequest).to.have.been.calledOnce()
+              expect(onData).to.have.been.called()
+              expect(fs.existsSync(fixtureLoc)).to.be.false()
 
               server.close(t.end)
             })
@@ -255,17 +256,16 @@ test('nockBack record tests', nw => {
   })
 
   nw.test('it records when configured correctly', t => {
-    t.plan(4)
+    const onRequest = sinon.spy()
 
     const fixture = 'someFixture.txt'
     const fixtureLoc = `${nockBack.fixtures}/${fixture}`
 
-    t.false(exists(fixtureLoc))
+    expect(fs.existsSync(fixtureLoc)).to.be.false()
 
-    nockBack(fixture, done => {
+    nockBack(fixture, nockDone => {
       const server = http.createServer((request, response) => {
-        t.pass('server received a request')
-
+        onRequest()
         response.writeHead(200)
         response.write('server served a response')
         response.end()
@@ -279,10 +279,11 @@ test('nockBack record tests', nw => {
             port: server.address().port,
           },
           response => {
-            done()
+            nockDone()
 
-            t.is(200, response.statusCode)
-            t.true(exists(fixtureLoc))
+            expect(onRequest).to.have.been.calledOnce()
+            expect(response.statusCode).to.equal(200)
+            expect(fs.existsSync(fixtureLoc)).to.be.true()
 
             fs.unlinkSync(fixtureLoc)
 
@@ -299,17 +300,16 @@ test('nockBack record tests', nw => {
   // Adding this test because there was an issue when not calling
   // nock.activate() after calling nock.restore().
   nw.test('it can record twice', t => {
-    t.plan(4)
+    const onRequest = sinon.spy()
 
     const fixture = 'someFixture2.txt'
     const fixtureLoc = `${nockBack.fixtures}/${fixture}`
 
-    t.false(exists(fixtureLoc))
+    expect(fs.existsSync(fixtureLoc)).to.be.false()
 
-    nockBack(fixture, function (done) {
+    nockBack(fixture, function (nockDone) {
       const server = http.createServer((request, response) => {
-        t.pass('server received a request')
-
+        onRequest()
         response.writeHead(200)
         response.write('server served a response')
         response.end()
@@ -323,10 +323,11 @@ test('nockBack record tests', nw => {
             port: server.address().port,
           },
           response => {
-            done()
+            nockDone()
 
-            t.is(200, response.statusCode)
-            t.true(exists(fixtureLoc))
+            expect(onRequest).to.have.been.calledOnce()
+            expect(response.statusCode).to.equal(200)
+            expect(fs.existsSync(fixtureLoc)).to.be.true()
 
             fs.unlinkSync(fixtureLoc)
 
@@ -341,46 +342,45 @@ test('nockBack record tests', nw => {
   })
 
   nw.test("it shouldn't allow outside calls", t => {
-    nockBack('wrong_uri.json', function (done) {
+    nockBack('wrong_uri.json', nockDone => {
       http
-        .get('http://other.example.test', res =>
-          t.fail('Should not come here!')
-        )
+        .get('http://other.example.test', res => expect.fail())
         .on('error', err => {
-          t.equal(
-            err.message,
+          expect(err.message).to.equal(
             'Nock: Disallowed net connect for "other.example.test:80/"'
           )
-          done()
+          nockDone()
           t.end()
         })
     })
   })
 
   nw.test('it loads your recorded tests', t => {
-    nockBack('good_request.json', function (done) {
-      t.true(this.scopes.length > 0)
-      http.get('http://www.example.test/').end()
-      this.assertScopesFinished()
-      done()
-      t.end()
+    nockBack('good_request.json', function (nockDone) {
+      expect(this.scopes).to.have.lengthOf.at.least(1)
+      http.get('http://www.example.test/', () => {
+        this.assertScopesFinished()
+        nockDone()
+        t.end()
+      })
     })
   })
 
   nw.test('it can filter after recording', t => {
+    const onRequest = sinon.spy()
+
     const fixture = 'filteredFixture.json'
     const fixtureLoc = `${nockBack.fixtures}/${fixture}`
 
-    t.false(exists(fixtureLoc))
+    expect(fs.existsSync(fixtureLoc)).to.be.false()
 
     // You would do some filtering here, but for this test we'll just return
     // an empty array.
     const afterRecord = scopes => []
 
-    nockBack(fixture, { afterRecord }, function (done) {
+    nockBack(fixture, { afterRecord }, function (nockDone) {
       const server = http.createServer((request, response) => {
-        t.pass('server received a request')
-
+        onRequest()
         response.writeHead(200)
         response.write('server served a response')
         response.end()
@@ -394,11 +394,12 @@ test('nockBack record tests', nw => {
             port: server.address().port,
           },
           response => {
-            done()
+            nockDone()
 
-            t.is(200, response.statusCode)
-            t.true(exists(fixtureLoc))
-            t.equal(this.scopes.length, 0)
+            expect(onRequest).to.have.been.calledOnce()
+            expect(response.statusCode).to.equal(200)
+            expect(fs.existsSync(fixtureLoc)).to.be.true()
+            expect(this.scopes).to.be.empty()
             fs.unlinkSync(fixtureLoc)
 
             server.close(t.end)
@@ -415,11 +416,11 @@ test('nockBack record tests', nw => {
     const fixture = 'filteredFixture.json'
     const fixtureLoc = `${nockBack.fixtures}/${fixture}`
 
-    t.false(exists(fixtureLoc))
+    expect(fs.existsSync(fixtureLoc)).to.be.false()
 
     const afterRecord = scopes => 'string-response'
 
-    nockBack(fixture, { afterRecord }, function (done) {
+    nockBack(fixture, { afterRecord }, function (nockDone) {
       const server = http.createServer((request, response) => {
         t.pass('server received a request')
 
@@ -436,11 +437,13 @@ test('nockBack record tests', nw => {
             port: server.address().port,
           },
           response => {
-            done()
+            nockDone()
 
-            t.is(200, response.statusCode)
-            t.true(exists(fixtureLoc))
-            t.is(fs.readFileSync(fixtureLoc, 'utf8'), 'string-response')
+            expect(response.statusCode).to.equal(200)
+            expect(fs.existsSync(fixtureLoc)).to.be.true()
+            expect(fs.readFileSync(fixtureLoc, 'utf8')).to.equal(
+              'string-response'
+            )
             fs.unlinkSync(fixtureLoc)
 
             server.close(t.end)
@@ -474,8 +477,7 @@ test('nockBack lockdown tests', nw => {
     )
 
     req.on('error', err => {
-      t.equal(
-        err.message.trim(),
+      expect(err.message.trim()).to.equal(
         'Nock: Disallowed net connect for "other.example.test:80/"'
       )
       t.end()
@@ -491,10 +493,11 @@ test('assertScopesFinished throws exception when Back still has pending scopes',
   nockBack.setMode('record')
   const fixtureName = 'good_request.json'
   const fixturePath = path.join(nockBack.fixtures, fixtureName)
-  nockBack(fixtureName, function (done) {
-    const expected = `["GET http://www.example.test:80/"] was not used, consider removing ${fixturePath} to rerecord fixture`
-    t.throws(() => this.assertScopesFinished(), { message: expected })
-    done()
+  nockBack(fixtureName, function (nockDone) {
+    expect(() => this.assertScopesFinished()).to.throw(
+      `["GET http://www.example.test:80/"] was not used, consider removing ${fixturePath} to rerecord fixture`
+    )
+    nockDone()
     t.end()
   })
 })
@@ -504,7 +507,7 @@ test('nockBack dryrun throws the expected exception when fs is not available', t
   nockBackWithoutFs.setMode('dryrun')
 
   nockBackWithoutFs.fixtures = `${__dirname}/fixtures`
-  t.throws(() => nockBackWithoutFs('good_request.json'), { message: 'no fs' })
+  expect(() => nockBackWithoutFs('good_request.json')).to.throw('no fs')
 
   t.end()
 })
@@ -514,6 +517,6 @@ test('nockBack record mode throws the expected exception when fs is not availabl
   nockBackWithoutFs.setMode('record')
 
   nockBackWithoutFs.fixtures = `${__dirname}/fixtures`
-  t.throws(() => nockBackWithoutFs('good_request.json'), { message: 'no fs' })
+  expect(() => nockBackWithoutFs('good_request.json')).to.throw('no fs')
   t.end()
 })
diff --git a/tests/test_back_2.js b/tests/test_back_2.js
index cfec696..f221e6b 100644
--- a/tests/test_back_2.js
+++ b/tests/test_back_2.js
@@ -2,12 +2,14 @@
 
 const http = require('http')
 const fs = require('fs')
+const sinon = require('sinon')
+const { expect } = require('chai')
 const { beforeEach, afterEach, test } = require('tap')
 const rimraf = require('rimraf')
 const nock = require('..')
+const { back: nockBack } = nock
 
-const nockBack = nock.back
-
+require('./setup')
 require('./cleanup_after_each')()
 
 const fixture = `${__dirname}/fixtures/recording_test.json`
@@ -26,12 +28,11 @@ afterEach(done => {
 })
 
 test('recording', t => {
-  t.plan(5)
+  const onRequest = sinon.spy()
 
   nockBack('recording_test.json', function (nockDone) {
     const server = http.createServer((request, response) => {
-      t.pass('server received a request')
-
+      onRequest()
       response.writeHead(301)
       response.write('server served a response')
       response.end()
@@ -50,15 +51,19 @@ test('recording', t => {
           response.once('end', () => {
             nockDone()
 
+            expect(onRequest).to.have.been.calledOnce()
+
             const fixtureContent = JSON.parse(
               fs.readFileSync(fixture, { encoding: 'utf8' })
             )
-            t.equal(fixtureContent.length, 1)
+            expect(fixtureContent).to.have.length(1)
 
             const [firstFixture] = fixtureContent
-            t.equal(firstFixture.method, 'GET')
-            t.equal(firstFixture.path, '/')
-            t.equal(firstFixture.status, 301)
+            expect(firstFixture).to.include({
+              method: 'GET',
+              path: '/',
+              status: 301,
+            })
 
             server.close(t.end)
           })
@@ -74,15 +79,14 @@ test('recording', t => {
 })
 
 test('passes custom options to recorder', t => {
-  t.plan(3)
+  const onRequest = sinon.spy()
 
   nockBack(
     'recording_test.json',
     { recorder: { enable_reqheaders_recording: true } },
     function (nockDone) {
       const server = http.createServer((request, response) => {
-        t.pass('server received a request')
-
+        onRequest()
         response.writeHead(200)
         response.write('server served a response')
         response.end()
@@ -100,12 +104,14 @@ test('passes custom options to recorder', t => {
             response.once('end', () => {
               nockDone()
 
+              expect(onRequest).to.have.been.calledOnce()
+
               const fixtureContent = JSON.parse(
                 fs.readFileSync(fixture, { encoding: 'utf8' })
               )
 
-              t.equal(fixtureContent.length, 1)
-              t.ok(fixtureContent[0].reqheaders)
+              expect(fixtureContent).to.have.length(1)
+              expect(fixtureContent[0].reqheaders).to.be.ok()
 
               server.close(t.end)
             })
diff --git a/tests/test_client_request.js b/tests/test_client_request.js
index 2607722..3f5b301 100644
--- a/tests/test_client_request.js
+++ b/tests/test_client_request.js
@@ -79,11 +79,10 @@ describe('Direct use of `ClientRequest`', () => {
     }
     const req = new http.ClientRequest(reqOpts, res => {
       expect(res.statusCode).to.equal(201)
+      scope.done()
       done()
     })
     req.end()
-
-    scope.done()
   })
 
   it('should throw an expected error when creating with empty options', () => {
diff --git a/tests/test_content_encoding.js b/tests/test_content_encoding.js
index 5867ec2..1ce80fd 100644
--- a/tests/test_content_encoding.js
+++ b/tests/test_content_encoding.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const assertRejects = require('assert-rejects')
 const { expect } = require('chai')
 const zlib = require('zlib')
 const nock = require('..')
@@ -27,11 +26,11 @@ describe('Content Encoding', () => {
     scope.done()
   })
 
-  it('Delaying the body is not available with content encoded responses', async () => {
+  it('Delaying the body works with content encoded responses', async () => {
     const message = 'Lorem ipsum dolor sit amet'
     const compressed = zlib.gzipSync(message)
 
-    nock('http://example.test')
+    const scope = nock('http://example.test')
       .get('/')
       .delay({
         body: 100,
@@ -40,9 +39,10 @@ describe('Content Encoding', () => {
         'Content-Encoding': 'gzip',
       })
 
-    await assertRejects(
-      got('http://example.test/'),
-      /Response delay of the body is currently not supported with content-encoded responses/
-    )
+    const { statusCode, body } = await got('http://example.test/')
+
+    expect(statusCode).to.equal(200)
+    expect(body).to.equal(message)
+    scope.done()
   })
 })
diff --git a/tests/test_define.js b/tests/test_define.js
index 7b345e3..5b4242c 100644
--- a/tests/test_define.js
+++ b/tests/test_define.js
@@ -1,15 +1,17 @@
 'use strict'
 
 const http = require('http')
+const { expect } = require('chai')
 const assertRejects = require('assert-rejects')
 const { test } = require('tap')
 const nock = require('..')
 const got = require('./got_client')
 
+require('./setup')
 require('./cleanup_after_each')()
 
 test('define() is backward compatible', async t => {
-  t.ok(
+  expect(
     nock.define([
       {
         scope: 'http://example.test',
@@ -21,39 +23,35 @@ test('define() is backward compatible', async t => {
         reply: '500',
       },
     ])
-  )
+  ).to.be.ok()
 
   await assertRejects(
     got('http://example.test:12345/'),
     ({ response: { statusCode } }) => {
-      t.is(statusCode, 500)
+      expect(statusCode).to.equal(500)
       return true
     }
   )
 })
 
 test('define() throws when reply is not a numeric string', t => {
-  t.throws(
-    () =>
-      nock.define([
-        {
-          scope: 'http://example.test:1451',
-          method: 'GET',
-          path: '/',
-          reply: 'frodo',
-        },
-      ]),
-    {
-      message: '`reply`, when present, must be a numeric string',
-    }
-  )
+  expect(() =>
+    nock.define([
+      {
+        scope: 'http://example.test:1451',
+        method: 'GET',
+        path: '/',
+        reply: 'frodo',
+      },
+    ])
+  ).to.throw('`reply`, when present, must be a numeric string')
   t.end()
 })
 
 test('define() applies default status code when none is specified', async t => {
   const body = '�'
 
-  t.equal(
+  expect(
     nock.define([
       {
         scope: 'http://example.test',
@@ -62,19 +60,18 @@ test('define() applies default status code when none is specified', async t => {
         body,
         response: '�',
       },
-    ]).length,
-    1
-  )
+    ])
+  ).to.have.lengthOf(1)
 
   const { statusCode } = await got.post('http://example.test/', { body })
 
-  t.equal(statusCode, 200)
+  expect(statusCode).to.equal(200)
 })
 
 test('define() works when scope and port are both specified', async t => {
   const body = 'Hello, world!'
 
-  t.ok(
+  expect(
     nock.define([
       {
         scope: 'http://example.test:1451',
@@ -85,52 +82,45 @@ test('define() works when scope and port are both specified', async t => {
         response: '�',
       },
     ])
-  )
+  ).to.be.ok()
 
   const { statusCode } = await got.post('http://example.test:1451/', { body })
 
-  t.equal(statusCode, 200)
+  expect(statusCode).to.equal(200)
 
   t.end()
 })
 
 test('define() throws the expected error when scope and port conflict', t => {
-  t.throws(
-    () =>
-      nock.define([
-        {
-          scope: 'http://example.test:8080',
-          port: 5000,
-          method: 'POST',
-          path: '/',
-          body: 'Hello, world!',
-          response: '�',
-        },
-      ]),
-    {
-      message:
-        'Mismatched port numbers in scope and port properties of nock definition.',
-    }
+  expect(() =>
+    nock.define([
+      {
+        scope: 'http://example.test:8080',
+        port: 5000,
+        method: 'POST',
+        path: '/',
+        body: 'Hello, world!',
+        response: '�',
+      },
+    ])
+  ).to.throw(
+    'Mismatched port numbers in scope and port properties of nock definition.'
   )
 
   t.end()
 })
 
 test('define() throws the expected error when method is missing', t => {
-  t.throws(
-    () =>
-      nock.define([
-        {
-          scope: 'http://example.test',
-          path: '/',
-          body: 'Hello, world!',
-          response: 'yo',
-        },
-      ]),
-    {
-      message: 'Method is required',
-    }
-  )
+  expect(() =>
+    nock.define([
+      {
+        scope: 'http://example.test',
+        path: '/',
+        body: 'Hello, world!',
+        response: 'yo',
+      },
+    ])
+  ).to.throw('Method is required')
 
   t.end()
 })
@@ -139,7 +129,7 @@ test('define() works with non-JSON responses', async t => {
   const exampleBody = '�'
   const exampleResponseBody = 'hey: �'
 
-  t.ok(
+  expect(
     nock.define([
       {
         scope: 'http://example.test',
@@ -150,16 +140,16 @@ test('define() works with non-JSON responses', async t => {
         response: exampleResponseBody,
       },
     ])
-  )
+  ).to.be.ok()
 
   const { statusCode, body } = await got.post('http://example.test/', {
     body: exampleBody,
     responseType: 'buffer',
   })
 
-  t.equal(statusCode, 200)
-  t.type(body, Buffer)
-  t.equal(body.toString(), exampleResponseBody)
+  expect(statusCode).to.equal(200)
+  expect(body).to.be.an.instanceOf(Buffer)
+  expect(body.toString()).to.equal(exampleResponseBody)
 })
 
 // TODO: There seems to be a bug here. When testing via `got` with
@@ -170,7 +160,7 @@ test('define() works with binary buffers', t => {
   const exampleBody = '8001'
   const exampleResponse = '8001'
 
-  t.ok(
+  expect(
     nock.define([
       {
         scope: 'http://example.test',
@@ -181,7 +171,7 @@ test('define() works with binary buffers', t => {
         response: exampleResponse,
       },
     ])
-  )
+  ).to.be.ok()
 
   const req = http.request(
     {
@@ -190,7 +180,7 @@ test('define() works with binary buffers', t => {
       path: '/',
     },
     res => {
-      t.equal(res.statusCode, 200)
+      expect(res.statusCode).to.equal(200)
 
       const dataChunks = []
 
@@ -200,7 +190,7 @@ test('define() works with binary buffers', t => {
 
       res.once('end', () => {
         const response = Buffer.concat(dataChunks)
-        t.equal(response.toString('hex'), exampleResponse, 'responses match')
+        expect(response.toString('hex')).to.equal(exampleResponse)
         t.end()
       })
     }
@@ -208,7 +198,7 @@ test('define() works with binary buffers', t => {
 
   req.on('error', () => {
     //  This should never happen.
-    t.fail('Error should never occur.')
+    expect.fail()
     t.end()
   })
 
@@ -224,7 +214,7 @@ test('define() uses reqheaders', t => {
     authorization: authHeader,
   }
 
-  t.ok(
+  expect(
     nock.define([
       {
         scope: 'http://example.test',
@@ -234,7 +224,7 @@ test('define() uses reqheaders', t => {
         reqheaders,
       },
     ])
-  )
+  ).to.be.ok()
 
   // Make a request which should match the mock that was configured above.
   // This does not hit the network.
@@ -246,10 +236,10 @@ test('define() uses reqheaders', t => {
       auth,
     },
     res => {
-      t.equal(res.statusCode, 200)
+      expect(res.statusCode).to.equal(200)
 
       res.once('end', () => {
-        t.equivalent(res.req.getHeaders(), reqheaders)
+        expect(res.req.getHeaders(), reqheaders)
         t.end()
       })
       // Streams start in 'paused' mode and must be started.
@@ -261,7 +251,7 @@ test('define() uses reqheaders', t => {
 })
 
 test('define() uses badheaders', t => {
-  t.ok(
+  expect(
     nock.define([
       {
         scope: 'http://example.test',
@@ -280,7 +270,7 @@ test('define() uses badheaders', t => {
         },
       },
     ])
-  )
+  ).to.be.ok()
 
   const req = http.request(
     {
@@ -292,7 +282,7 @@ test('define() uses badheaders', t => {
       },
     },
     res => {
-      t.equal(res.statusCode, 200)
+      expect(res.statusCode).to.equal(200)
 
       res.once('end', () => {
         t.end()
@@ -304,29 +294,3 @@ test('define() uses badheaders', t => {
   )
   req.end()
 })
-
-test('define() treats a * body as a special case for not matching the request body', async t => {
-  t.plan(4)
-
-  nock.define([
-    {
-      scope: 'http://example.test',
-      method: 'POST',
-      path: '/',
-      body: '*',
-      response: 'matched',
-    },
-  ])
-
-  process.once('warning', warning => {
-    t.equal(warning.name, 'DeprecationWarning')
-    t.match(warning.message, 'Skipping body matching using')
-  })
-
-  const { statusCode, body } = await got.post('http://example.test/', {
-    body: 'foo bar',
-  })
-
-  t.equal(statusCode, 200)
-  t.equal(body, 'matched')
-})
diff --git a/tests/test_delay.js b/tests/test_delay.js
index 4f86979..5357ecc 100644
--- a/tests/test_delay.js
+++ b/tests/test_delay.js
@@ -7,11 +7,9 @@ const http = require('http')
 const stream = require('stream')
 const assertRejects = require('assert-rejects')
 const sinon = require('sinon')
-const { test } = require('tap')
 const nock = require('..')
 const got = require('./got_client')
 
-require('./cleanup_after_each')()
 require('./setup')
 
 const textFilePath = path.resolve(__dirname, './assets/reply_file_1.txt')
@@ -40,289 +38,342 @@ function checkDuration(start, durationMillis) {
   // .and.at.most(durationMillis + bufferMillis, 'delay upper bound exceeded')
 }
 
-test('calling delay could cause timeout error', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delay({
-      head: 300,
-    })
-    .reply(200, 'OK')
+describe('`delay()`', () => {
+  let interceptor
+  let connSpy
+  let bodySpy
+
+  // As a rule, the tests in this repo have a strategy of only testing the API and not spying on
+  // internals for unit tests. These next few tests break that rule to assert the proxy behavior of
+  // `delay()`. This is simply to reduce the need of double testing the behavior of `delayBody()`
+  // and `delayConnection()` and should not be used as an example for writing new tests.
+  beforeEach(() => {
+    interceptor = nock('http://example.test').get('/')
+    connSpy = sinon.spy(interceptor, 'delayConnection')
+    bodySpy = sinon.spy(interceptor, 'delayBody')
+  })
 
-  await assertRejects(
-    got('http://example.test', { timeout: 100 }),
-    err => err.code === 'ETIMEDOUT'
-  )
+  it('should proxy a single number argument', () => {
+    interceptor.delay(42)
 
-  scope.done()
-})
+    expect(connSpy).to.have.been.calledOnceWithExactly(42)
+    expect(bodySpy).to.have.been.calledOnceWithExactly(0)
+  })
 
-test('Body delay does not have impact on timeout', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delay({
-      head: 300,
-      body: 300,
-    })
-    .reply(201, 'OK')
+  it('should proxy values from an object argument', () => {
+    interceptor.delay({ head: 42, body: 17 })
+
+    expect(connSpy).to.have.been.calledOnceWithExactly(42)
+    expect(bodySpy).to.have.been.calledOnceWithExactly(17)
+  })
+
+  it('should default missing values from an object argument', () => {
+    interceptor.delay({})
 
-  const { body, statusCode } = await got('http://example.test/', {
-    timeout: {
-      response: 500,
-    },
+    expect(connSpy).to.have.been.calledOnceWithExactly(0)
+    expect(bodySpy).to.have.been.calledOnceWithExactly(0)
   })
 
-  expect(statusCode).to.equal(201)
-  expect(body).to.equal('OK')
-  scope.done()
+  it('should throw on invalid arguments', () => {
+    expect(() => interceptor.delay('one million seconds')).to.throw(
+      'Unexpected input'
+    )
+  })
+
+  it('should delay the response when called with "body" and "head"', done => {
+    nock('http://example.test')
+      .get('/')
+      .delay({
+        head: 200,
+        body: 300,
+      })
+      .reply(200, 'OK')
+
+    const start = process.hrtime()
+
+    http.get('http://example.test', res => {
+      checkDuration(start, 200)
+
+      res.once('data', function (data) {
+        checkDuration(start, 500)
+        expect(data.toString()).to.equal('OK')
+        res.once('end', done)
+      })
+    })
+  })
 })
 
-test('calling delay with "body" and "head" delays the response', t => {
-  nock('http://example.test')
-    .get('/')
-    .delay({
-      head: 200,
-      body: 300,
+describe('`delayBody()`', () => {
+  it('should delay the clock between the `response` event and the first `data` event', done => {
+    nock('http://example.test').get('/').delayBody(200).reply(201, 'OK')
+
+    http.get('http://example.test', res => {
+      const start = process.hrtime()
+      res.once('data', () => {
+        checkDuration(start, 200)
+        done()
+      })
     })
-    .reply(200, 'OK')
+  })
 
-  const resStart = process.hrtime()
+  it('should delay the overall response', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayBody(200)
+      .reply(200, 'OK')
 
-  http.get('http://example.test', res => {
-    checkDuration(resStart, 200)
+    const { body } = await resolvesInAtLeast(got('http://example.test/'), 200)
 
-    // const dataStart = process.hrtime()
-    res.once('data', function (data) {
-      // TODO: there is a bug in Nock that allows this delay to be less than the desired 300ms.
-      //  there is a known issues with streams, but this needs further investigation.
-      // checkDuration(dataStart, 300)
-      expect(data.toString()).to.equal('OK')
-      res.once('end', () => t.done())
-    })
+    expect(body).to.equal('OK')
+    scope.done()
   })
-})
 
-test('calling delay with "body" delays the response body', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delay({ body: 200 })
-    .reply(200, 'OK')
+  it('should not have an impact on a response timeout', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(300)
+      .delayBody(300)
+      .reply(201, 'OK')
+
+    const { body, statusCode } = await got('http://example.test/', {
+      timeout: {
+        response: 500,
+      },
+    })
 
-  const { body } = await resolvesInAtLeast(got('http://example.test/'), 200)
+    expect(statusCode).to.equal(201)
+    expect(body).to.equal('OK')
+    scope.done()
+  })
 
-  expect(body).to.equal('OK')
-  scope.done()
-})
+  it('should work with a response stream', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayBody(200)
+      .reply(200, () => fs.createReadStream(textFilePath, { encoding: 'utf8' }))
 
-test('calling delayBody delays the response', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayBody(200)
-    .reply(200, 'OK')
+    const { body } = await resolvesInAtLeast(got('http://example.test/'), 200)
 
-  const { body } = await resolvesInAtLeast(got('http://example.test/'), 200)
+    expect(body).to.equal(textFileContents)
+    scope.done()
+  })
 
-  expect(body).to.equal('OK')
-  scope.done()
-})
+  it('should work with a response stream of binary buffers', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayBody(200)
+      // No encoding specified, which causes the file to be streamed using
+      // buffers instead of strings.
+      .reply(200, () => fs.createReadStream(textFilePath))
 
-test('delayBody works with a stream', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayBody(200)
-    .reply(200, () => fs.createReadStream(textFilePath, { encoding: 'utf8' }))
+    const { body } = await resolvesInAtLeast(got('http://example.test/'), 200)
 
-  const { body } = await resolvesInAtLeast(got('http://example.test/'), 200)
+    expect(body).to.equal(textFileContents)
+    scope.done()
+  })
 
-  expect(body).to.equal(textFileContents)
-  scope.done()
-})
+  it('should work with a delayed response stream', async () => {
+    const passthrough = new stream.Transform({
+      transform(chunk, encoding, callback) {
+        this.push(chunk.toString())
+        callback()
+      },
+    })
 
-test('delayBody works with a stream of binary buffers', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayBody(200)
-    // No encoding specified, which causes the file to be streamed using
-    // buffers instead of strings.
-    .reply(200, () => fs.createReadStream(textFilePath))
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayBody(100)
+      .reply(200, () => passthrough)
 
-  const { body } = await resolvesInAtLeast(got('http://example.test/'), 200)
+    setTimeout(() => fs.createReadStream(textFilePath).pipe(passthrough), 125)
 
-  expect(body).to.equal(textFileContents)
-  scope.done()
+    const { body } = await got('http://example.test/')
+
+    expect(body).to.equal(textFileContents)
+    scope.done()
+  })
 })
 
-test('delayBody works with a delayed stream', async () => {
-  const passthrough = new stream.Transform({
-    transform(chunk, encoding, callback) {
-      this.push(chunk.toString())
-      callback()
-    },
+describe('`delayConnection()`', () => {
+  it('should cause a timeout error when larger than options.timeout', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(1000)
+      .reply(200, {})
+
+    await assertRejects(
+      got('http://example.test', { timeout: 10 }),
+      err => err.code === 'ETIMEDOUT'
+    )
+
+    scope.done()
   })
 
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayBody(100)
-    .reply(200, () => passthrough)
+  it('should delay the clock before the `response` event', done => {
+    nock('http://example.test').get('/').delayConnection(200).reply()
 
-  setTimeout(() => fs.createReadStream(textFilePath).pipe(passthrough), 125)
+    const req = http.request('http://example.test', () => {
+      checkDuration(start, 200)
+      done()
+    })
 
-  const { body } = await got('http://example.test/')
+    req.end()
+    const start = process.hrtime()
+  })
 
-  expect(body).to.equal(textFileContents)
-  scope.done()
-})
+  it('should delay the overall response', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(200)
+      .reply(200, 'OK')
 
-test('calling delay delays the response', async () => {
-  const scope = nock('http://example.test').get('/').delay(200).reply(200, 'OK')
+    const { body } = await resolvesInAtLeast(got('http://example.test'), 200)
 
-  const { body } = await resolvesInAtLeast(got('http://example.test'), 200)
+    expect(body).to.equal('OK')
+    scope.done()
+  })
 
-  expect(body).to.equal('OK')
-  scope.done()
-})
+  it('should provide the proper arguments when using reply a callback', async () => {
+    const replyStub = sinon.stub().returns('')
 
-test('using reply callback with delay provides proper arguments', async () => {
-  const replyStub = sinon.stub().returns('')
+    const scope = nock('http://example.test')
+      .post('/')
+      .delayConnection(100)
+      .reply(200, replyStub)
 
-  const scope = nock('http://example.test')
-    .post('/')
-    .delay(100)
-    .reply(200, replyStub)
+    await got.post('http://example.test', { body: 'OK' })
 
-  await got.post('http://example.test', { body: 'OK' })
+    expect(replyStub).to.have.been.calledOnceWithExactly('/', 'OK')
+    scope.done()
+  })
 
-  expect(replyStub).to.have.been.calledOnceWithExactly('/', 'OK')
-  scope.done()
-})
+  it('should delay a JSON response when using a reply callback', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(100)
+      .reply(200, () => ({ a: 1 }))
 
-test('using reply callback with delay can reply JSON', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delay(100)
-    .reply(200, () => ({ a: 1 }))
+    const { body, headers, statusCode } = await got('http://example.test')
 
-  const { body, headers, statusCode } = await got('http://example.test')
+    expect(body).to.equal('{"a":1}')
+    expect(headers).to.have.property('content-type', 'application/json')
+    expect(statusCode).to.equal(200)
+    scope.done()
+  })
 
-  expect(body).to.equal('{"a":1}')
-  expect(headers).to.have.property('content-type', 'application/json')
-  expect(statusCode).to.equal(200)
-  scope.done()
-})
+  it('should work with `replyWithFile()`', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(200)
+      .replyWithFile(200, textFilePath)
 
-test('delay with invalid arguments', t => {
-  const interceptor = nock('http://example.test').get('/')
+    const { body } = await resolvesInAtLeast(got('http://example.test'), 200)
 
-  expect(() => interceptor.delay('one million seconds')).to.throw(
-    'Unexpected input'
-  )
-  t.end()
-})
+    expect(body).to.equal(textFileContents)
+    scope.done()
+  })
 
-test('delay works with replyWithFile', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delay(200)
-    .replyWithFile(200, textFilePath)
+  it('should work with a generic stream from the reply callback', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(200)
+      .reply(200, () => fs.createReadStream(textFilePath))
 
-  const { body, statusCode } = await resolvesInAtLeast(
-    got('http://example.test'),
-    200
-  )
+    const { body } = await resolvesInAtLeast(got('http://example.test'), 200)
 
-  expect(statusCode).to.equal(200)
-  expect(body).to.equal(textFileContents)
-  scope.done()
-})
+    expect(body).to.equal(textFileContents)
+    scope.done()
+  })
 
-test('delay works with when you return a generic stream from the reply callback', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delay(200)
-    .reply(200, () => fs.createReadStream(textFilePath))
+  it('should work with a generic stream from the reply callback', async () => {
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(200)
+      .reply(200, () => fs.createReadStream(textFilePath))
 
-  const { body, statusCode } = await resolvesInAtLeast(
-    got('http://example.test'),
-    200
-  )
+    const { body, statusCode } = await resolvesInAtLeast(
+      got('http://example.test'),
+      200
+    )
 
-  expect(statusCode).to.equal(200)
-  expect(body).to.equal(textFileContents)
-  scope.done()
-})
+    expect(statusCode).to.equal(200)
+    expect(body).to.equal(textFileContents)
+    scope.done()
+  })
 
-test('delay with replyWithError: response is delayed', async () => {
-  nock('http://example.test')
-    .get('/')
-    .delay(100)
-    .replyWithError('this is an error message')
+  it('should delay errors when `replyWithError()` is used', async () => {
+    nock('http://example.test')
+      .get('/')
+      .delayConnection(100)
+      .replyWithError('this is an error message')
 
-  await resolvesInAtLeast(
-    assertRejects(got('http://example.test'), /this is an error message/),
-    100
-  )
-})
+    await resolvesInAtLeast(
+      assertRejects(got('http://example.test'), /this is an error message/),
+      100
+    )
+  })
 
-test('calling delayConnection delays the connection', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayConnection(200)
-    .reply(200, 'OK')
+  it('emits a timeout - with setTimeout', done => {
+    nock('http://example.test').get('/').delayConnection(10000).reply(200, 'OK')
 
-  const { body } = await resolvesInAtLeast(got('http://example.test'), 200)
+    const onEnd = sinon.spy()
 
-  expect(body).to.equal('OK')
-  scope.done()
-})
+    const req = http.request('http://example.test', res => {
+      res.once('end', onEnd)
+    })
+
+    req.setTimeout(5000, () => {
+      expect(onEnd).not.to.have.been.called()
+      done()
+    })
 
-test('using reply callback with delayConnection provides proper arguments', async () => {
-  const replyStub = sinon.stub().returns('')
+    req.end()
+  })
 
-  const scope = nock('http://example.test')
-    .post('/')
-    .delayConnection(100)
-    .reply(200, replyStub)
+  it('emits a timeout - with options.timeout', done => {
+    nock('http://example.test').get('/').delayConnection(10000).reply(200, 'OK')
 
-  await got.post('http://example.test', { body: 'OK' })
+    const onEnd = sinon.spy()
 
-  expect(replyStub).to.have.been.calledOnceWithExactly('/', 'OK')
-  scope.done()
-})
+    const req = http.request('http://example.test', { timeout: 5000 }, res => {
+      res.once('end', onEnd)
+    })
 
-test('delayConnection works with replyWithFile', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayConnection(200)
-    .replyWithFile(200, textFilePath)
+    req.on('timeout', function () {
+      expect(onEnd).not.to.have.been.called()
+      done()
+    })
 
-  const { body } = await resolvesInAtLeast(got('http://example.test'), 200)
+    req.end()
+  })
 
-  expect(body).to.equal(textFileContents)
-  scope.done()
-})
+  it('does not emit a timeout when timeout > delayConnection', done => {
+    const responseText = 'okeydoke!'
+    const scope = nock('http://example.test')
+      .get('/')
+      .delayConnection(300)
+      .reply(200, responseText)
 
-test('delayConnection works with when you return a generic stream from the reply callback', async () => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayConnection(200)
-    .reply(200, () => fs.createReadStream(textFilePath))
+    const req = http.request('http://example.test', res => {
+      res.setEncoding('utf8')
 
-  const { body } = await resolvesInAtLeast(got('http://example.test'), 200)
+      let body = ''
 
-  expect(body).to.equal(textFileContents)
-  scope.done()
-})
+      res.on('data', chunk => {
+        body += chunk
+      })
 
-test('request with delayConnection and request.timeout', async t => {
-  const scope = nock('http://example.test')
-    .get('/')
-    .delayConnection(1000)
-    .reply(200, {})
+      res.once('end', () => {
+        expect(body).to.equal(responseText)
+        scope.done()
+        done()
+      })
+    })
 
-  await assertRejects(
-    got('http://example.test', { timeout: 10 }),
-    err => err.code === 'ETIMEDOUT'
-  )
+    req.setTimeout(60000, () => {
+      expect.fail('socket timed out unexpectedly')
+    })
 
-  scope.done()
-  t.done()
+    req.end()
+  })
 })
diff --git a/tests/test_events.js b/tests/test_events.js
index 6ee3380..74cc9d4 100644
--- a/tests/test_events.js
+++ b/tests/test_events.js
@@ -2,6 +2,7 @@
 
 const { expect } = require('chai')
 const http = require('http')
+const path = require('path')
 const sinon = require('sinon')
 
 const nock = require('..')
@@ -38,7 +39,7 @@ it('emits request and request body', async () => {
   scope.on('request', function (req, interceptor, body) {
     onRequest()
     expect(req.path).to.equal('/please')
-    expect(interceptor.interceptionCounter).to.equal(0)
+    expect(interceptor.interceptionCounter).to.equal(1)
     expect(body).to.deep.equal(data)
     expect(onReplied).to.not.have.been.called()
   })
@@ -62,6 +63,25 @@ it('emits request and request body', async () => {
   expect(onReplied).to.have.been.calledOnce()
 })
 
+it('emits request and replied events when response body is a stream', async () => {
+  const textFilePath = path.resolve(__dirname, './assets/reply_file_1.txt')
+  const scope = nock('http://example.test')
+    .get('/')
+    .replyWithFile(200, textFilePath)
+
+  const onRequest = sinon.spy()
+  const onReplied = sinon.spy()
+
+  scope.on('request', onRequest)
+  scope.on('replied', onReplied)
+
+  await got('http://example.test')
+
+  scope.done()
+  expect(onRequest).to.have.been.calledOnce()
+  expect(onReplied).to.have.been.calledOnce()
+})
+
 it('emits no match when no match and no mock', done => {
   nock.emitter.once('no match', () => {
     done()
diff --git a/tests/test_header_matching.js b/tests/test_header_matching.js
index b41e3aa..e346733 100644
--- a/tests/test_header_matching.js
+++ b/tests/test_header_matching.js
@@ -568,7 +568,7 @@ it('header manipulation', done => {
     .get('/accounts')
     .reply(200, { accounts: [{ id: 1, name: 'Joe Blow' }] })
 
-  const req = http.get({ host: 'example.test', path: '/accounts' }, res => {
+  const req = http.request({ host: 'example.test', path: '/accounts' }, res => {
     res.on('end', () => {
       scope.done()
       done()
diff --git a/tests/test_intercept.js b/tests/test_intercept.js
index 90a5988..ec024fb 100644
--- a/tests/test_intercept.js
+++ b/tests/test_intercept.js
@@ -255,34 +255,6 @@ test('on interceptor, filter path with function', async t => {
   scope.done()
 })
 
-// TODO: Move to test_request_overrider.
-test('abort request', t => {
-  const scope = nock('http://example.test').get('/hey').reply(200, 'nobody')
-
-  const req = http.request({
-    host: 'example.test',
-    path: '/hey',
-  })
-
-  req.on('response', res => {
-    res.on('close', err => {
-      expect(err.code).to.equal('aborted')
-      scope.done()
-    })
-
-    res.on('end', () => expect.fail())
-
-    req.once('error', err => {
-      expect(err.code).to.equal('ECONNRESET')
-      t.end()
-    })
-
-    req.abort()
-  })
-
-  req.end()
-})
-
 test('chaining API', async t => {
   const scope = nock('http://example.test')
     .get('/one')
@@ -708,27 +680,19 @@ test('get correct filtering with scope and request headers filtering', t => {
     .reply(200, responseText, { 'Content-Type': 'text/plain' })
 
   const onData = sinon.spy()
-  const req = http.get(
-    {
-      host: 'bar.example.test',
-      method: 'GET',
-      path: '/path',
-      port: 80,
-    },
-    res => {
-      res.on('data', data => {
-        onData()
-        expect(data.toString()).to.equal(responseText)
-      })
-      res.on('end', () => {
-        expect(onData).to.have.been.calledOnce()
-        scope.done()
-        t.end()
-      })
-    }
-  )
+  const req = http.get('http://bar.example.test/path', res => {
+    expect(req.getHeaders()).to.deep.equal({ host: requestHeaders.host })
 
-  expect(req.getHeaders()).to.deep.equal({ host: requestHeaders.host })
+    res.on('data', data => {
+      onData()
+      expect(data.toString()).to.equal(responseText)
+    })
+    res.on('end', () => {
+      expect(onData).to.have.been.calledOnce()
+      scope.done()
+      t.end()
+    })
+  })
 })
 
 test('different subdomain with reply callback and filtering scope', async t => {
diff --git a/tests/test_logging.js b/tests/test_logging.js
index fe42ca8..f600014 100644
--- a/tests/test_logging.js
+++ b/tests/test_logging.js
@@ -12,11 +12,10 @@ describe('Logging using the `debug` package', () => {
   let logFn
   beforeEach(() => {
     logFn = sinon.stub(debug, 'log')
-    debug.enable('nock.interceptor')
+    debug.enable('nock*')
   })
   afterEach(() => {
-    sinon.restore()
-    debug.disable('nock.interceptor')
+    debug.disable('nock*')
   })
 
   it('match debugging works', async () => {
@@ -26,36 +25,39 @@ describe('Logging using the `debug` package', () => {
     await got.post('http://example.test/deep/link', { body: exampleBody })
 
     const isMocha = process.argv.some(arg => arg.endsWith('mocha'))
-    // TODO For some reason this is getting slightly different arugments in Tap
+    // TODO For some reason this is getting slightly different arguments in Tap
     // vs Mocha. Remove this conditional when Tap is removed.
     if (isMocha) {
+      // the log function will have been a few dozen times, there are a few specific to matching we want to validate:
+
+      // the log when an interceptor is chosen
       expect(logFn).to.have.been.calledWith(
-        sinon.match.string,
+        sinon.match('matched base path (1 interceptor)')
+      )
+
+      // the log of the Interceptor match
+      expect(logFn).to.have.been.calledWith(
+        // debug namespace for the scope that includes the host
+        sinon.match('nock.scope:example.test'),
         // This is a JSON blob which contains, among other things the complete
         // request URL.
         sinon.match('"href":"http://example.test/deep/link"'),
         // This is the JSON-stringified body.
         `"${exampleBody}"`
       )
-    }
-  })
-})
-
-describe('`log()`', () => {
-  it('should log host matching', async () => {
-    const logFn = sinon.spy()
-
-    const scope = nock('http://example.test')
-      .get('/')
-      .reply(200, 'Hello, World!')
-      .log(logFn)
 
-    await got('http://example.test/')
-
-    expect(logFn).to.have.been.calledOnceWithExactly(
-      'matching http://example.test:80/ to GET http://example.test:80/: true'
-    )
+      expect(logFn).to.have.been.calledWith(
+        sinon.match('query matching skipped')
+      )
 
-    scope.done()
+      expect(logFn).to.have.been.calledWith(
+        sinon.match(
+          'matching http://example.test:80/deep/link to POST http://example.test:80/deep/link: true'
+        )
+      )
+      expect(logFn).to.have.been.calledWith(
+        sinon.match('interceptor identified, starting mocking')
+      )
+    }
   })
 })
diff --git a/tests/test_nock_lifecycle.js b/tests/test_nock_lifecycle.js
index d5be210..0ef6873 100644
--- a/tests/test_nock_lifecycle.js
+++ b/tests/test_nock_lifecycle.js
@@ -106,6 +106,25 @@ describe('Nock lifecycle functions', () => {
         return true
       })
     })
+
+    it('should be safe to call in the middle of a request', done => {
+      // This covers a race-condition where cleanAll() is called while a request
+      // is in mid-flight. The request itself should continue to process normally.
+      // Notably, `cleanAll` is being called before the Interceptor is marked as
+      // consumed and removed from the global map. Having this test wait until the
+      // response event means we verify it didn't throw an error when attempting
+      // to remove an Interceptor that doesn't exist in the global map `allInterceptors`.
+      nock('http://example.test').get('/').reply()
+
+      const req = http.request('http://example.test', () => {
+        done()
+      })
+      req.once('socket', () => {
+        nock.cleanAll()
+      })
+
+      req.end()
+    })
   })
 
   describe('`isDone()`', () => {
diff --git a/tests/test_reply_headers.js b/tests/test_reply_headers.js
index 59d2832..34f2529 100644
--- a/tests/test_reply_headers.js
+++ b/tests/test_reply_headers.js
@@ -235,8 +235,7 @@ describe('`reply()` headers', () => {
             myHeaderFnCalled()
             expect(req).to.be.an.instanceof(OverriddenClientRequest)
             expect(res).to.be.an.instanceof(IncomingMessage)
-            expect(body).to.be.an.instanceof(Buffer)
-            expect(Buffer.from('boo!').equals(body)).to.be.true()
+            expect(body).to.equal('boo!')
             return 'gotcha'
           },
         })
diff --git a/tests/test_request_overrider.js b/tests/test_request_overrider.js
index ad2b1d5..9def185 100644
--- a/tests/test_request_overrider.js
+++ b/tests/test_request_overrider.js
@@ -50,9 +50,10 @@ describe('Request Overrider', () => {
 
     const req = http.get('http://example.test')
 
-    scope.done()
-
-    req.on('response', () => done())
+    req.on('response', () => {
+      scope.done()
+      done()
+    })
   })
 
   it('write callback called', done => {
@@ -220,6 +221,21 @@ describe('Request Overrider', () => {
     req.end('foobar', reqEndCallback)
   })
 
+  it('should emit an error if `write` is called after `end`', done => {
+    nock('http://example.test').get('/').reply()
+
+    const req = http.request('http://example.test')
+
+    req.on('error', err => {
+      expect(err.message).to.equal('write after end')
+      expect(err.code).to.equal('ERR_STREAM_WRITE_AFTER_END')
+      done()
+    })
+
+    req.end()
+    req.write('foo')
+  })
+
   // http://github.com/nock/nock/issues/139
   it('should emit "finish" on the request before emitting "end" on the response', done => {
     const scope = nock('http://example.test').post('/').reply()
@@ -538,12 +554,31 @@ describe('Request Overrider', () => {
     nock('http://example.test').get('/').reply(200, 'hey')
 
     const req = http.get('http://example.test')
+    req.on('error', () => {}) // listen for error so it doesn't bubble
     req.once('socket', socket => {
       socket.destroy()
       done()
     })
   })
 
+  it('calling Socket#destroy() multiple times only emits a single `close` event', done => {
+    nock('http://example.test').get('/').reply(200, 'hey')
+
+    const req = http.get('http://example.test')
+    req.on('error', () => {}) // listen for error so it doesn't bubble
+    req.once('socket', socket => {
+      const closeSpy = sinon.spy()
+      socket.on('close', closeSpy)
+
+      socket.destroy().destroy().destroy()
+
+      setTimeout(() => {
+        expect(closeSpy).to.have.been.calledOnce()
+        done()
+      }, 10)
+    })
+  })
+
   it('socket has getPeerCertificate() method which returns a random base64 string', done => {
     nock('http://example.test').get('/').reply()
 
diff --git a/tests/test_socket.js b/tests/test_socket.js
new file mode 100644
index 0000000..bcc3ee8
--- /dev/null
+++ b/tests/test_socket.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const http = require('http')
+const nock = require('..')
+
+require('./setup')
+
+describe('`Socket#setTimeout()`', () => {
+  it('adds callback as a one-time listener for parity with a real socket', done => {
+    nock('http://example.test').get('/').delayConnection(100).reply()
+
+    const onTimeout = () => {
+      done()
+    }
+
+    http.get('http://example.test').on('socket', socket => {
+      socket.setTimeout(50, onTimeout)
+    })
+  })
+
+  it('can be called without a callback', done => {
+    nock('http://example.test').get('/').delayConnection(100).reply()
+
+    http.get('http://example.test').on('socket', socket => {
+      socket.setTimeout(50)
+
+      socket.on('timeout', () => {
+        done()
+      })
+    })
+  })
+})
diff --git a/tests/test_socket_delay.js b/tests/test_socket_delay.js
deleted file mode 100644
index a275339..0000000
--- a/tests/test_socket_delay.js
+++ /dev/null
@@ -1,132 +0,0 @@
-'use strict'
-
-const http = require('http')
-const { expect } = require('chai')
-const sinon = require('sinon')
-const nock = require('..')
-
-require('./setup')
-
-describe('`socketDelay()`', () => {
-  it('socketDelay', done => {
-    nock('http://example.test')
-      .get('/')
-      .socketDelay(200)
-      .reply(200, '<html></html>')
-
-    const req = http.get('http://example.test')
-
-    const onTimeout = sinon.spy()
-
-    req.on('socket', socket => {
-      if (!socket.connecting) {
-        req.setTimeout(100, onTimeout)
-        return
-      }
-
-      socket.on('connect', () => {
-        req.setTimeout(100, onTimeout)
-      })
-    })
-
-    req.on('response', () => {
-      expect(onTimeout).to.have.been.calledOnce()
-      done()
-    })
-  })
-
-  it('emits a timeout - with setTimeout', done => {
-    nock('http://example.test').get('/').socketDelay(10000).reply(200, 'OK')
-
-    const onEnd = sinon.spy()
-
-    const req = http.request('http://example.test', res => {
-      res.setEncoding('utf8')
-      res.once('end', onEnd)
-    })
-
-    req.setTimeout(5000, () => {
-      expect(onEnd).not.to.have.been.called()
-      done()
-    })
-
-    req.end()
-  })
-
-  it('emits a timeout - with options.timeout', done => {
-    nock('http://example.test').get('/').socketDelay(10000).reply(200, 'OK')
-
-    const onEnd = sinon.spy()
-
-    const req = http.request('http://example.test', { timeout: 5000 }, res => {
-      res.setEncoding('utf8')
-      res.once('end', onEnd)
-    })
-
-    req.on('timeout', function () {
-      expect(onEnd).not.to.have.been.called()
-      done()
-    })
-
-    req.end()
-  })
-
-  it('does not emit a timeout when timeout > socketDelay', done => {
-    const responseText = 'okeydoke!'
-    const scope = nock('http://example.test')
-      .get('/')
-      .socketDelay(10000)
-      .reply(200, responseText)
-
-    const req = http.request('http://example.test', res => {
-      res.setEncoding('utf8')
-
-      let body = ''
-
-      res.on('data', chunk => {
-        body += chunk
-      })
-
-      res.once('end', () => {
-        expect(body).to.equal(responseText)
-        done()
-      })
-    })
-
-    req.setTimeout(60000, () => {
-      expect.fail('socket timed out unexpectedly')
-    })
-
-    req.end()
-    scope.done()
-  })
-})
-
-describe('`Socket#setTimeout()`', () => {
-  it('adds callback as a one-time listener for parity with a real socket', done => {
-    nock('http://example.test')
-      .get('/')
-      .socketDelay(100)
-      .reply(200, '<html></html>')
-
-    const onTimeout = () => {
-      done()
-    }
-
-    http.get('http://example.test').on('socket', socket => {
-      socket.setTimeout(50, onTimeout)
-    })
-  })
-
-  it('can be called without a callback', done => {
-    nock('http://example.test').get('/').socketDelay(100).reply()
-
-    http.get('http://example.test').on('socket', socket => {
-      socket.setTimeout(50)
-
-      socket.on('timeout', () => {
-        done()
-      })
-    })
-  })
-})
diff --git a/tests/test_stream.js b/tests/test_stream.js
index 7b38c71..50fb0be 100644
--- a/tests/test_stream.js
+++ b/tests/test_stream.js
@@ -43,35 +43,29 @@ it('pause response after data', done => {
     // multiple 'data' events.
     .reply(200, response)
 
-  http.get(
-    {
-      host: 'example.test',
-      path: '/',
-    },
-    res => {
-      const didTimeout = sinon.spy()
+  http.get('http://example.test', res => {
+    const didTimeout = sinon.spy()
 
-      setTimeout(() => {
-        didTimeout()
-        res.resume()
-      }, 500)
+    setTimeout(() => {
+      didTimeout()
+      res.resume()
+    }, 500)
 
-      res.on('data', data => res.pause())
+    res.on('data', () => res.pause())
 
-      res.on('end', () => {
-        expect(didTimeout).to.have.been.calledOnce()
-        scope.done()
-        done()
-      })
-    }
-  )
+    res.on('end', () => {
+      expect(didTimeout).to.have.been.calledOnce()
+      scope.done()
+      done()
+    })
 
-  // Manually simulate multiple 'data' events.
-  response.emit('data', 'one')
-  setTimeout(() => {
-    response.emit('data', 'two')
-    response.end()
-  }, 0)
+    // Manually simulate multiple 'data' events.
+    response.emit('data', 'one')
+    setTimeout(() => {
+      response.emit('data', 'two')
+      response.end()
+    }, 0)
+  })
 })
 
 // https://github.com/nock/nock/issues/1493
@@ -83,30 +77,22 @@ it("response has 'complete' property and it's true after end", done => {
     // multiple 'data' events.
     .reply(200, response)
 
-  http.get(
-    {
-      host: 'example.test',
-      path: '/',
-    },
-    res => {
-      const onData = sinon.spy()
+  http.get('http://example.test', res => {
+    const onData = sinon.spy()
 
-      res.on('data', onData)
+    res.on('data', onData)
 
-      res.on('end', () => {
-        expect(onData).to.have.been.called()
-        expect(res.complete).to.be.true()
-        scope.done()
-        done()
-      })
-    }
-  )
+    res.on('end', () => {
+      expect(onData).to.have.been.called()
+      expect(res.complete).to.be.true()
+      scope.done()
+      done()
+    })
 
-  // Manually simulate multiple 'data' events.
-  response.emit('data', 'one')
-  setTimeout(() => {
+    // Manually simulate multiple 'data' events.
+    response.emit('data', 'one')
     response.end()
-  }, 0)
+  })
 })
 
 // TODO Convert to async / got.
@@ -325,14 +311,13 @@ it('error events on reply streams proxy to the response', done => {
     res => {
       res.on('error', err => {
         expect(err).to.equal('oh no!')
+        scope.done()
         done()
       })
+
+      replyBody.end(() => {
+        replyBody.emit('error', 'oh no!')
+      })
     }
   )
-
-  scope.done()
-
-  replyBody.end(() => {
-    replyBody.emit('error', 'oh no!')
-  })
 })
diff --git a/types/tests.ts b/types/tests.ts
index a6886c0..cfc8701 100644
--- a/types/tests.ts
+++ b/types/tests.ts
@@ -120,7 +120,6 @@ scope = scope.filteringRequestBody((path: string) => {
   return str
 })
 
-scope = scope.log(() => {})
 scope = scope.persist()
 scope = scope.persist(false)
 scope = scope.replyContentLength()
@@ -131,7 +130,6 @@ inst = inst.delay(2000)
 inst = inst.delay({ head: 1000, body: 1000 })
 inst = inst.delayBody(2000)
 inst = inst.delayConnection(2000)
-inst = inst.socketDelay(2000)
 
 scope.done() // $ExpectType void
 scope.isDone() // $ExpectType boolean
@@ -503,13 +501,6 @@ nock('http://example.test')
   })
   .reply(200, '<html></html>')
 
-// Delay the connection
-nock('http://example.test')
-  .get('/')
-  .socketDelay(2000) // 2 seconds
-  .delayConnection(1000)
-  .reply(200, '<html></html>')
-
 // Chaining
 scope = nock('http://example.test')
   .get('/users/1')
@@ -619,7 +610,7 @@ options = { allowUnmocked: true }
 scope = nock('http://example.test', options).get('/my/url').reply(200, 'OK!')
 
 // Expectations
-let google = nock('http://example.test')
+const google = nock('http://example.test')
   .get('/')
   .reply(200, 'Hello from Google!')
 setTimeout(() => {
@@ -653,9 +644,6 @@ console.error('pending mocks: %j', nock.pendingMocks())
 nock.activeMocks() // $ExpectType string[]
 nock('http://example.test').activeMocks() // $ExpectType string[]
 
-// Logging
-google = nock('http://example.test').log(console.log)
-
 // Restoring
 nock.restore()