Comparing version 13.0.0-beta.1 to 13.0.0-beta.2
'use strict' | ||
const _ = require('lodash') | ||
const debug = require('debug')('nock.common') | ||
const set = require('lodash.set') | ||
const timers = require('timers') | ||
const url = require('url') | ||
const timers = require('timers') | ||
const util = require('util') | ||
@@ -197,3 +198,3 @@ /** | ||
function headersFieldNamesToLowerCase(headers) { | ||
if (!_.isPlainObject(headers)) { | ||
if (!isPlainObject(headers)) { | ||
throw Error('Headers must be provided as an object') | ||
@@ -247,7 +248,7 @@ } | ||
// [].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)) | ||
@@ -364,3 +365,3 @@ } | ||
function deleteHeadersField(headers, fieldNameToDelete) { | ||
if (!_.isPlainObject(headers)) { | ||
if (!isPlainObject(headers)) { | ||
throw Error('headers must be an object') | ||
@@ -565,3 +566,3 @@ } | ||
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), {}) | ||
@@ -579,3 +580,3 @@ /** | ||
if (Array.isArray(expected) || _.isPlainObject(expected)) { | ||
if (Array.isArray(expected) || isPlainObject(expected)) { | ||
if (actual === undefined) { | ||
@@ -596,2 +597,47 @@ return false | ||
/** | ||
* 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 = [] | ||
@@ -623,27 +669,31 @@ const intervals = [] | ||
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, | ||
} |
@@ -42,5 +42,10 @@ 'use strict' | ||
this.response = new IncomingMessage(this.socket) | ||
this.requestBodyBuffers = [] | ||
this.playbackStarted = false | ||
this.requestBodyBuffers = [] | ||
// 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() | ||
@@ -50,6 +55,4 @@ } | ||
attachToReq() { | ||
const { req, response, socket, options } = this | ||
const { req, socket, options } = this | ||
response.req = req | ||
for (const [name, val] of Object.entries(options.headers)) { | ||
@@ -76,3 +79,3 @@ req.setHeader(name.toLowerCase(), val) | ||
propagate(['error', 'timeout'], req.socket, req) | ||
propagate(['close', 'error', 'timeout'], req.socket, req) | ||
@@ -96,2 +99,7 @@ req.write = (...args) => this.handleWrite(...args) | ||
process.nextTick(() => { | ||
if (req.aborted) { | ||
return | ||
} | ||
socket.connecting = false | ||
req.emit('socket', socket) | ||
@@ -106,2 +114,6 @@ | ||
} | ||
if (this.readyToStartPlaybackOnSocketEvent) { | ||
this.maybeStartPlayback() | ||
} | ||
}) | ||
@@ -117,2 +129,4 @@ } | ||
// 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) { | ||
@@ -122,20 +136,34 @@ debug('write', arguments) | ||
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' | ||
this.emitError(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.aborted) { | ||
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 () { | ||
@@ -152,3 +180,3 @@ req.emit('drain') | ||
// handle the different overloaded param signatures | ||
// handle the different overloaded arg signatures | ||
if (typeof chunk === 'function') { | ||
@@ -166,9 +194,5 @@ callback = chunk | ||
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() | ||
@@ -180,10 +204,3 @@ return req | ||
debug('req.flushHeaders') | ||
const { req } = this | ||
if (!req.aborted && !this.playbackStarted) { | ||
this.startPlayback() | ||
} | ||
if (req.aborted) { | ||
this.emitError(new Error('Request aborted')) | ||
} | ||
this.maybeStartPlayback() | ||
} | ||
@@ -193,3 +210,3 @@ | ||
debug('req.abort') | ||
const { req, response, socket } = this | ||
const { req, socket } = this | ||
@@ -199,17 +216,25 @@ if (req.aborted) { | ||
} | ||
req.aborted = Date.now() | ||
if (!this.playbackStarted) { | ||
this.startPlayback() | ||
} | ||
const err = new Error() | ||
err.code = 'aborted' | ||
response.emit('close', err) | ||
socket.destroy() | ||
// Historically, `aborted` was undefined or a timestamp. | ||
// Node changed this behavior in v11 to always be a bool. | ||
req.aborted = true | ||
req.destroyed = true | ||
req.emit('abort') | ||
// the order of these next three steps is important to match order of events Node would emit. | ||
process.nextTick(() => req.emit('abort')) | ||
const connResetError = new Error('socket hang up') | ||
connResetError.code = 'ECONNRESET' | ||
this.emitError(connResetError) | ||
if (!socket.connecting) { | ||
if (!req.res) { | ||
// 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. | ||
// Node doesn't actually emit this during the `abort` method. | ||
// Instead it listens to the socket's `end` and `close` events, however, | ||
// Nock's socket doesn't have all the complexities and events to | ||
// replicated that directly. This is an easy way to achieve the same behavior. | ||
const connResetError = new Error('socket hang up') | ||
connResetError.code = 'ECONNRESET' | ||
this.emitError(connResetError) | ||
} | ||
socket.destroy() | ||
} | ||
} | ||
@@ -248,2 +273,17 @@ | ||
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 | ||
} | ||
if (!req.aborted && !playbackStarted) { | ||
this.startPlayback() | ||
} | ||
} | ||
startPlayback() { | ||
@@ -290,3 +330,2 @@ debug('ending') | ||
// otherwise an unmocked request might emit finish twice. | ||
req.finished = true | ||
req.emit('finish') | ||
@@ -293,0 +332,0 @@ |
@@ -5,3 +5,2 @@ 'use strict' | ||
const stringify = require('json-stringify-safe') | ||
const _ = require('lodash') | ||
const querystring = require('querystring') | ||
@@ -487,3 +486,3 @@ const { URL, URLSearchParams } = require('url') | ||
queries = querystring.parse(queries.toString()) | ||
} else if (!_.isPlainObject(queries)) { | ||
} else if (!common.isPlainObject(queries)) { | ||
throw Error(`Argument Error: ${queries}`) | ||
@@ -490,0 +489,0 @@ } |
'use strict' | ||
const _ = require('lodash') | ||
const querystring = require('querystring') | ||
@@ -73,6 +72,6 @@ | ||
} | ||
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) | ||
} |
@@ -260,6 +260,2 @@ 'use strict' | ||
if (req.aborted) { | ||
return | ||
} | ||
response.rawHeaders.push( | ||
@@ -281,15 +277,9 @@ ...selectDefaultHeaders( | ||
process.nextTick(() => | ||
respondUsingInterceptor({ | ||
responseBody, | ||
responseBuffers, | ||
}) | ||
) | ||
respondUsingInterceptor({ | ||
responseBody, | ||
responseBuffers, | ||
}) | ||
} | ||
function respondUsingInterceptor({ responseBody, responseBuffers }) { | ||
if (req.aborted) { | ||
return | ||
} | ||
function respond() { | ||
@@ -300,2 +290,8 @@ if (req.aborted) { | ||
// 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 | ||
debug('emitting response') | ||
@@ -348,5 +344,14 @@ req.emit('response', response) | ||
start() | ||
// If there are no delays configured on the Interceptor, calling `start` will | ||
// take the request all the way to the 'response' event 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 } |
@@ -21,3 +21,3 @@ 'use strict' | ||
this.destroyed = false | ||
this.connecting = false | ||
this.connecting = true | ||
@@ -73,10 +73,26 @@ // totalDelay that has already been applied to the current | ||
/** | ||
* 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 | ||
} | ||
this.destroyed = true | ||
this.readable = this.writable = false | ||
if (err) { | ||
this.emit('error', err) | ||
} | ||
process.nextTick(() => { | ||
if (err) { | ||
this.emit('error', err) | ||
} | ||
this.emit('close') | ||
}) | ||
return this | ||
} | ||
} |
@@ -10,3 +10,3 @@ { | ||
], | ||
"version": "13.0.0-beta.1", | ||
"version": "13.0.0-beta.2", | ||
"author": "Pedro Teixeira <pedro.teixeira@gmail.com>", | ||
@@ -28,3 +28,3 @@ "repository": { | ||
"json-stringify-safe": "^5.0.1", | ||
"lodash": "^4.17.13", | ||
"lodash.set": "^4.3.2", | ||
"propagate": "^2.0.0" | ||
@@ -31,0 +31,0 @@ }, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
173653
3474
+ Addedlodash.set@^4.3.2
+ Addedlodash.set@4.3.2(transitive)
- Removedlodash@^4.17.13
- Removedlodash@4.17.21(transitive)