nock
Version:
HTTP server mocking and expectations library for Node.js
285 lines (234 loc) • 8.31 kB
JavaScript
import { STATUS_CODES } from 'node:http'
import stream from 'node:stream'
import util from 'node:util'
import { playback_interceptor as debug } from './debug.js'
import * as common from './common.js'
import { FetchResponse } from '@mswjs/interceptors'
function parseFullReplyResult(fullReplyResult ) {
debug('full response from callback result: %j', fullReplyResult)
if (!Array.isArray(fullReplyResult)) {
throw Error('A single function provided to .reply MUST return an array')
}
if (fullReplyResult.length > 3) {
throw Error(
'The array returned from the .reply callback contains too many values',
)
}
const [status, body = '', headers] = fullReplyResult
if (!Number.isInteger(status)) {
throw new Error(`Invalid ${typeof status} value for status code`)
}
const rawHeaders = common.headersInputToRawArray(headers)
debug('response.rawHeaders after reply: %j', rawHeaders)
return [status, body, rawHeaders]
}
function selectDefaultHeaders(existingHeaders , defaultHeaders ) {
if (!defaultHeaders.length) {
return [] // return early if we don't need to bother
}
const definedHeaders = new Set()
const result = []
common.forEachHeader(existingHeaders, (_ , fieldName ) => {
definedHeaders.add(fieldName.toLowerCase())
})
common.forEachHeader(defaultHeaders, (value , fieldName ) => {
if (!definedHeaders.has(fieldName.toLowerCase())) {
result.push([fieldName, value])
}
})
return result
}
// Presents a list of Buffers as a Readable
class ReadableBuffers extends stream.Readable {
constructor(buffers ) {
super()
this.buffers = buffers
}
_read() {
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)])
}
async function playbackInterceptor({
decompressedRequest,
interceptor,
requestBodyString,
requestBodyIsUtf8Representable,
}
) {
const { logger } = interceptor.scope
interceptor.scope.emit(
'request',
decompressedRequest,
interceptor,
requestBodyString,
)
if (typeof interceptor.errorMessage !== 'undefined') {
let error
if (typeof interceptor.errorMessage === 'object') {
error = interceptor.errorMessage
} else {
error = new Error(interceptor.errorMessage)
}
await new Promise(resolve =>
common.setTimeout(resolve, interceptor.delayBodyInMs),
)
throw error
}
logger('response.rawHeaders:', interceptor.rawHeaders)
// .reply(status, replyFunction)
if (interceptor.replyFunction) {
let fn = interceptor.replyFunction
if (fn.length === 2) {
// Handle the case of an async reply function, the third parameter being the callback.
fn = util.promisify(fn)
}
// 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.
return Promise.resolve((fn ).call(interceptor, decompressedRequest))
.then(continueWithResponseBody)
.catch((err ) => {
throw err
})
}
// .reply(fullReplyFunction)
else if (interceptor.fullReplyFunction) {
let fn = interceptor.fullReplyFunction
if (fn.length === 2) {
fn = util.promisify(fn)
}
return Promise.resolve((fn ).call(interceptor, decompressedRequest))
.then(continueWithFullResponse)
.catch((err ) => {
throw err
})
}
if (
common.isContentEncoded(interceptor.headers || {}) &&
!common.isStream(interceptor.body)
) {
// If the content is encoded we know that the response body *must* be an array
// of response buffers which should be mocked one by one.
const bufferData = Array.isArray(interceptor.body)
? interceptor.body
: [interceptor.body]
const responseBuffers = bufferData.map((data ) =>
Buffer.from(data, 'hex'),
)
const responseBody = new ReadableBuffers(responseBuffers)
return continueWithResponseBody(responseBody)
}
// If we get to this point, the body is either a string or an object that
// will eventually be JSON stringified.
let responseBody = interceptor.body
// If the request was not UTF8-representable then we assume that the
// response won't be either. In that case we send the response as a Buffer
// object as that's what the client will expect.
if (!requestBodyIsUtf8Representable && typeof responseBody === 'string') {
// Try to create the buffer from the interceptor's body response as hex.
responseBody = Buffer.from(responseBody, 'hex')
// Creating buffers does not necessarily throw errors; check for difference in size.
if (
!responseBody ||
((interceptor.body ).length > 0 && responseBody.length === 0)
) {
// We fallback on constructing buffer from utf8 representation of the body.
responseBody = Buffer.from(interceptor.body , 'utf8')
}
}
return continueWithResponseBody(responseBody)
function continueWithFullResponse(fullReplyResult ) {
const [status, responseBody, rawHeaders] =
parseFullReplyResult(fullReplyResult)
return continueWithResponseBody(responseBody, status, rawHeaders)
}
async function prepareResponseHeaders(body , responseHeaders ) {
const defaultHeaders = [...interceptor.scope._defaultReplyHeaders]
const rawHeaders = []
// Include a JSON content type when JSON.stringify is called on the body.
const isJSON =
body !== undefined &&
typeof body !== 'string' &&
!Buffer.isBuffer(body) &&
!common.isStream(body)
if (isJSON) {
defaultHeaders.push('Content-Type', 'application/json')
}
common.forEachHeader(
[...(interceptor.rawHeaders || []), ...responseHeaders],
(value , fieldName ) => {
rawHeaders.push([fieldName, value])
},
)
rawHeaders.push(
...selectDefaultHeaders(
[...(interceptor.rawHeaders || []), ...responseHeaders],
defaultHeaders,
),
)
for (let i = 0; i < rawHeaders.length; i++) {
const [, value] = rawHeaders[i]
// Evaluate functional headers.
if (typeof value === 'function') {
rawHeaders[i][1] = await value(decompressedRequest, body)
}
}
return new Headers(rawHeaders)
}
async function continueWithResponseBody(
rawBody ,
fullResponseStatus ,
fullResponseRawHeaders = [],
) {
const headers = await prepareResponseHeaders(
rawBody,
fullResponseRawHeaders,
)
const bodyAsStream = convertBodyToStream(rawBody)
bodyAsStream.resume()
// TODO: there is probably a better way to support delay.
// Wrap the stream in a duplex stream to support the delay.
const readable = new stream.Readable({
read() {},
})
bodyAsStream.on('data', function (chunk ) {
readable.push(chunk)
})
bodyAsStream.on('end', function () {
common.setTimeout(() => {
readable.push(null)
interceptor.scope.emit('replied', decompressedRequest, interceptor)
}, interceptor.delayBodyInMs)
})
bodyAsStream.on('error', function (err ) {
readable.emit('error', err)
})
const status = interceptor.statusCode || fullResponseStatus
const hasBody = FetchResponse.isResponseWithBody(status )
return new Response(hasBody ? readable : null, {
status: status ,
statusText: STATUS_CODES[status ],
headers,
})
}
}
export { playbackInterceptor }