Skip to content

Commit ef3f158

Browse files
mikichogr2mMichael SolomonUzlopakdependabot[bot]
authored
feat: support for native fetch (#2813)
BREAKING CHANGE: drop support for Node < 18 Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> Co-authored-by: Michael Solomon <micheal540@gmail.com> Co-authored-by: Aras Abbasi <aras.abbasi@googlemail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Haruaki OTAKE <aaharu@hotmail.com> Co-authored-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Johannes Pfrang <johannespfrang@gmail.com> Co-authored-by: Chris M <821688+tebriel@users.noreply.github.com>
1 parent c8b2b22 commit ef3f158

42 files changed

Lines changed: 3186 additions & 13805 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ env:
22
node: true
33

44
parserOptions:
5-
ecmaVersion: 9
5+
ecmaVersion: 2020
66
# Override eslint-config-standard, which incorrectly sets this to "module",
77
# though that setting is only for ES6 modules, not CommonJS modules.
88
sourceType: 'script'

.github/workflows/continuous-integration.yaml

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ name: Continuous Integration
66
- synchronize
77
push:
88
branches:
9+
- '*.x'
910
- main
11+
- beta
12+
- next
1013
permissions:
1114
contents: read
1215

@@ -22,7 +25,7 @@ jobs:
2225
- name: Setup Node
2326
uses: actions/setup-node@v4
2427
with:
25-
node-version: 16
28+
node-version: lts/*
2629
cache: 'npm'
2730
- name: Install dependencies
2831
run: npm ci --ignore-scripts --no-audit --no-progress --prefer-offline
@@ -70,23 +73,13 @@ jobs:
7073
fail-fast: false
7174
matrix:
7275
node-version:
73-
- 10
74-
- 12
75-
- 14
76-
- 16
7776
- 18
7877
- 20
78+
- 22
7979
os:
8080
- macos-latest
8181
- ubuntu-latest
8282
- windows-latest
83-
exclude:
84-
- node-version: 10
85-
os: macos-latest
86-
- node-version: 12
87-
os: macos-latest
88-
- node-version: 14
89-
os: macos-latest
9083
runs-on: ${{ matrix.os }}
9184
timeout-minutes: 5
9285

@@ -106,7 +99,6 @@ jobs:
10699
run: npm run test
107100
- name: Test jest
108101
run: npm run test:jest
109-
if: matrix.node-version >= 14
110102

111103
# separate job to set as required in branch protection,
112104
# as the build names above change each time Node versions change

.nycrc.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
all: true
22
check-coverage: true
33

4-
branches: 100
5-
lines: 100
4+
branches: 94.59
5+
lines: 96.89
66

77
include:
88
- lib

README.md

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
11
# Nock
22

33
[![npm](https://img.shields.io/npm/v/nock.svg)][npmjs]
4-
[![Build Status](https://travis-ci.org/nock/nock.svg)][build]
54
![Coverage Status](http://img.shields.io/badge/coverage-100%25-brightgreen.svg)
65
[![Backers on Open Collective](https://opencollective.com/nock/backers/badge.svg)](#backers)
76
[![Sponsors on Open Collective](https://opencollective.com/nock/sponsors/badge.svg)](#sponsors)
87

98
[npmjs]: https://www.npmjs.com/package/nock
10-
[build]: https://travis-ci.org/nock/nock
11-
12-
> **Notice**
13-
>
14-
> We have introduced experimental support for fetch. Please share your feedback with us. You can install it by:
15-
>
16-
> ```
17-
> npm install --save-dev nock@beta
18-
> ```
199

2010
HTTP server mocking and expectations library for Node.js
2111

@@ -694,46 +684,17 @@ You are able to specify the number of milliseconds that your reply should be del
694684
```js
695685
nock('http://my.server.com')
696686
.get('/')
697-
.delay(2000) // 2 seconds delay will be applied to the response header.
687+
.delay(2000) // 2 seconds delay will be applied to the response body.
698688
.reply(200, '<html></html>')
699689
```
700690

701-
`delay(1000)` is an alias for `delayConnection(1000).delayBody(0)`
702-
`delay({ head: 1000, body: 2000 })` is an alias for `delayConnection(1000).delayBody(2000)`
703-
Both of which are covered in detail below.
704-
705691
#### Delay the connection
706692

707-
You are able to specify the number of milliseconds that your connection should be idle before it starts to receive the response.
708-
709-
To simulate a socket timeout, provide a larger value than the timeout setting on the request.
710-
711-
```js
712-
nock('http://my.server.com')
713-
.get('/')
714-
.delayConnection(2000) // 2 seconds
715-
.reply(200, '<html></html>')
716-
717-
req = http.request('http://my.server.com', { timeout: 1000 })
718-
```
719-
720-
Nock emits timeout events almost immediately by comparing the requested connection delay to the timeout parameter passed to `http.request()` or `http.ClientRequest#setTimeout()`.
721-
This allows you to test timeouts without using fake timers or slowing down your tests.
722-
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.
723-
724-
##### Technical Details
725-
726-
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.
727-
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.
728-
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.
729-
730-
A similar method, `.socketDelay()` was removed in version 13. It was thought that having two methods so subtlety similar was confusing.
731-
The discussion can be found at https://github.com/nock/nock/pull/1974.
693+
The `delayConnection` method’s behavior of emitting quick timeout events when the connection delay exceeds the request timeout is now deprecated. Please use the `delay` function instead.
732694

733695
#### Delay the response body
734696

735-
You are able to specify the number of milliseconds that the response body should be delayed.
736-
This is the time between the headers being received and the body starting to be received.
697+
The `delayBody` is now deprecated. Please use the `delay` function instead.
737698

738699
```js
739700
nock('http://my.server.com')
@@ -1643,10 +1604,10 @@ It does this by manipulating the modules cache of Node in a way that conflicts w
16431604

16441605
## Debugging
16451606

1646-
Nock uses [`debug`](https://github.com/visionmedia/debug), so just run with environmental variable `DEBUG` set to `nock.*`.
1607+
Nock uses node internals [`debuglog`](https://nodejs.org/api/util.html#utildebuglogsection-callbackg), so just run with environmental variable `NODE_DEBUG` set to `nock:*`.
16471608

16481609
```console
1649-
user@local$ DEBUG=nock.* node my_test.js
1610+
user@local$ NODE_DEBUG=nock:* node my_test.js
16501611
```
16511612

16521613
Each step in the matching process is logged this way and can be useful when determining why a request was not intercepted by Nock.
@@ -1660,11 +1621,11 @@ await got('http://example.com/?foo=bar&baz=foz')
16601621
```
16611622

16621623
```console
1663-
user@local$ DEBUG=nock.scope:example.com node my_test.js
1624+
user@local$ DEBUG=nock:scope:example.com node my_test.js
16641625
...
1665-
nock.scope:example.com Interceptor queries: {"foo":"bar"} +1ms
1666-
nock.scope:example.com Request queries: {"foo":"bar","baz":"foz"} +0ms
1667-
nock.scope:example.com query matching failed +0ms
1626+
NOCK:SCOPE:EXAMPLE.COM 103514: Interceptor queries: {"foo":"bar"}
1627+
NOCK:SCOPE:EXAMPLE.COM 103514: Request queries: {"foo":"bar","baz":"foz"}
1628+
NOCK:SCOPE:EXAMPLE.COM 103514: query matching failed
16681629
```
16691630

16701631
## Contributing

lib/back.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ const {
99
removeAll: cleanAll,
1010
} = require('./intercept')
1111
const { loadDefs, define } = require('./scope')
12-
12+
const { back: debug } = require('./debug')
1313
const { format } = require('util')
1414
const path = require('path')
15-
const debug = require('debug')('nock.back')
1615

1716
let _mode = null
1817

@@ -78,7 +77,6 @@ function Back(fixtureName, options, nockedFn) {
7877
}
7978

8079
debug('context:', context)
81-
8280
// If nockedFn is a function then invoke it, otherwise return a promise resolving to nockDone.
8381
if (typeof nockedFn === 'function') {
8482
nockedFn.call(context, nockDone)

lib/common.js

Lines changed: 29 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use strict'
22

3-
const debug = require('debug')('nock.common')
3+
const { common: debug } = require('./debug')
44
const timers = require('timers')
55
const url = require('url')
66
const util = require('util')
7+
const http = require('http')
78

89
/**
910
* Normalizes the request options so that it always has `host` property.
@@ -50,82 +51,6 @@ function isUtf8Representable(buffer) {
5051
return reconstructedBuffer.equals(buffer)
5152
}
5253

53-
// Array where all information about all the overridden requests are held.
54-
let requestOverrides = {}
55-
56-
/**
57-
* Overrides the current `request` function of `http` and `https` modules with
58-
* our own version which intercepts issues HTTP/HTTPS requests and forwards them
59-
* to the given `newRequest` function.
60-
*
61-
* @param {Function} newRequest - a function handling requests; it accepts four arguments:
62-
* - proto - a string with the overridden module's protocol name (either `http` or `https`)
63-
* - overriddenRequest - the overridden module's request function already bound to module's object
64-
* - options - the options of the issued request
65-
* - callback - the callback of the issued request
66-
*/
67-
function overrideRequests(newRequest) {
68-
debug('overriding requests')
69-
;['http', 'https'].forEach(function (proto) {
70-
debug('- overriding request for', proto)
71-
72-
const moduleName = proto // 1 to 1 match of protocol and module is fortunate :)
73-
const module = require(proto)
74-
const overriddenRequest = module.request
75-
const overriddenGet = module.get
76-
77-
if (requestOverrides[moduleName]) {
78-
throw new Error(
79-
`Module's request already overridden for ${moduleName} protocol.`,
80-
)
81-
}
82-
83-
// Store the properties of the overridden request so that it can be restored later on.
84-
requestOverrides[moduleName] = {
85-
module,
86-
request: overriddenRequest,
87-
get: overriddenGet,
88-
}
89-
// https://nodejs.org/api/http.html#http_http_request_url_options_callback
90-
module.request = function (input, options, callback) {
91-
return newRequest(proto, overriddenRequest.bind(module), [
92-
input,
93-
options,
94-
callback,
95-
])
96-
}
97-
// https://nodejs.org/api/http.html#http_http_get_options_callback
98-
module.get = function (input, options, callback) {
99-
const req = newRequest(proto, overriddenGet.bind(module), [
100-
input,
101-
options,
102-
callback,
103-
])
104-
req.end()
105-
return req
106-
}
107-
108-
debug('- overridden request for', proto)
109-
})
110-
}
111-
112-
/**
113-
* Restores `request` function of `http` and `https` modules to values they
114-
* held before they were overridden by us.
115-
*/
116-
function restoreOverriddenRequests() {
117-
debug('restoring requests')
118-
Object.entries(requestOverrides).forEach(
119-
([proto, { module, request, get }]) => {
120-
debug('- restoring request for', proto)
121-
module.request = request
122-
module.get = get
123-
debug('- restored request for', proto)
124-
},
125-
)
126-
requestOverrides = {}
127-
}
128-
12954
/**
13055
* In WHATWG URL vernacular, this returns the origin portion of a URL.
13156
* However, the port is not included if it's standard and not already present on the host.
@@ -621,6 +546,7 @@ function clearTimer(clear, ids) {
621546
}
622547

623548
function removeAllTimers() {
549+
debug('remove all timers')
624550
clearTimer(clearTimeout, timeouts)
625551
clearTimer(clearImmediate, immediates)
626552
}
@@ -653,6 +579,31 @@ function isRequestDestroyed(req) {
653579
)
654580
}
655581

582+
/**
583+
* @param {Request} request
584+
*/
585+
function convertFetchRequestToClientRequest(request) {
586+
const url = new URL(request.url)
587+
const options = {
588+
...urlToOptions(url),
589+
method: request.method,
590+
host: url.hostname,
591+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
592+
path: url.pathname + url.search,
593+
proto: url.protocol.slice(0, -1),
594+
headers: Object.fromEntries(request.headers.entries()),
595+
}
596+
597+
// By default, Node adds a host header, but for maximum backward compatibility, we are now removing it.
598+
// However, we need to consider leaving the header and fixing the tests.
599+
if (options.headers.host === options.host) {
600+
const { host, ...restHeaders } = options.headers
601+
options.headers = restHeaders
602+
}
603+
604+
return new http.ClientRequest(options)
605+
}
606+
656607
/**
657608
* Returns true if the given value is a plain object and not an Array.
658609
* @param {*} value
@@ -760,12 +711,11 @@ module.exports = {
760711
normalizeClientRequestArgs,
761712
normalizeOrigin,
762713
normalizeRequestOptions,
763-
overrideRequests,
764714
percentDecode,
765715
percentEncode,
766716
removeAllTimers,
767-
restoreOverriddenRequests,
768717
setImmediate,
769718
setTimeout,
770719
stringifyRequest,
720+
convertFetchRequestToClientRequest,
771721
}

lib/create_response.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict'
2+
3+
const { STATUS_CODES } = require('http')
4+
5+
/**
6+
* Creates a Fetch API `Response` instance from the given
7+
* `http.IncomingMessage` instance.
8+
* Inspired by: https://github.com/mswjs/interceptors/blob/04152ed914f8041272b6e92ed374216b8177e1b2/src/interceptors/ClientRequest/utils/createResponse.ts#L8
9+
*/
10+
11+
/**
12+
* Response status codes for responses that cannot have body.
13+
* @see https://fetch.spec.whatwg.org/#statuses
14+
*/
15+
const responseStatusCodesWithoutBody = [204, 205, 304]
16+
17+
/**
18+
* @param {import('http').IncomingMessage} message
19+
* @param {AbortSignal} signal
20+
*/
21+
function createResponse(message, signal) {
22+
const responseBodyOrNull = responseStatusCodesWithoutBody.includes(
23+
message.statusCode || 200,
24+
)
25+
? null
26+
: new ReadableStream({
27+
start(controller) {
28+
message.on('data', chunk => controller.enqueue(chunk))
29+
message.on('end', () => controller.close())
30+
message.on('error', error => controller.error(error))
31+
signal.addEventListener('abort', () => message.destroy(signal.reason))
32+
},
33+
cancel() {
34+
message.destroy()
35+
},
36+
})
37+
38+
const rawHeaders = new Headers()
39+
for (let i = 0; i < message.rawHeaders.length; i += 2) {
40+
rawHeaders.append(message.rawHeaders[i], message.rawHeaders[i + 1])
41+
}
42+
43+
// @mswjs/interceptors supports rawHeaders. https://github.com/mswjs/interceptors/pull/598
44+
const response = new Response(responseBodyOrNull, {
45+
status: message.statusCode,
46+
statusText: message.statusMessage || STATUS_CODES[message.statusCode],
47+
headers: rawHeaders,
48+
})
49+
50+
return response
51+
}
52+
53+
module.exports = { createResponse }

0 commit comments

Comments
 (0)