Skip to content

Commit

Permalink
Merge branch 'main' into cookie-store
Browse files Browse the repository at this point in the history
  • Loading branch information
KhafraDev committed May 25, 2022
2 parents bf8d96b + 6855006 commit ce0abac
Show file tree
Hide file tree
Showing 34 changed files with 952 additions and 198 deletions.
4 changes: 3 additions & 1 deletion docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,18 +194,20 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
* **method** `string`
* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
* **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
* **query** `Record<string, any> | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead.
* **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed.
* **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
* **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
* **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds.
* **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.

#### Parameter: `DispatchHandler`

* **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
* **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
* **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
* **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
* **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests.
* **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
* **onBodySent** `(chunk: string | Buffer | Uint8Array) => void` - Invoked when a body chunk is sent to the server. Not required. For a stream or iterable body this will be invoked for every chunk. For other body types, it will be invoked once after the body is sent.
Expand Down
32 changes: 32 additions & 0 deletions docs/best-practices/mocking-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,36 @@ const badRequest = await bankTransfer('1234567890', '100')
// subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
```

## Reply with data based on request

If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`

```js
mockPool.intercept({
path: '/bank-transfer',
method: 'POST',
headers: {
'X-TOKEN-SECRET': 'SuperSecretToken',
},
body: JSON.stringify({
recepient: '1234567890',
amount: '100'
})
}).reply(200, (opts) => {
// do something with opts

return { message: 'transaction processed' }
})
```

in this case opts will be

```
{
method: 'POST',
headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
body: '{"recepient":"1234567890","amount":"100"}',
origin: 'http://localhost:3000',
path: '/bank-transfer'
}
```
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { request, pipeline, stream, connect, upgrade } from './types/api'
export * from './types/fetch'
export * from './types/file'
export * from './types/formdata'
export * from './types/diagnostics-channel'
export { Interceptable } from './types/mock-interceptor'

export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
Expand Down
15 changes: 12 additions & 3 deletions lib/api/api-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
const Readable = require('./readable')
const {
InvalidArgumentError,
RequestAbortedError
RequestAbortedError,
ResponseStatusCodeError
} = require('../core/errors')
const util = require('../core/util')
const { AsyncResource } = require('async_hooks')
Expand All @@ -15,7 +16,7 @@ class RequestHandler extends AsyncResource {
throw new InvalidArgumentError('invalid opts')
}

const { signal, method, opaque, body, onInfo, responseHeaders } = opts
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts

try {
if (typeof callback !== 'function') {
Expand Down Expand Up @@ -51,6 +52,7 @@ class RequestHandler extends AsyncResource {
this.trailers = {}
this.context = null
this.onInfo = onInfo || null
this.throwOnError = throwOnError

if (util.isStream(body)) {
body.on('error', (err) => {
Expand All @@ -70,7 +72,7 @@ class RequestHandler extends AsyncResource {
this.context = context
}

onHeaders (statusCode, rawHeaders, resume) {
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
const { callback, opaque, abort, context } = this

if (statusCode < 200) {
Expand All @@ -89,6 +91,13 @@ class RequestHandler extends AsyncResource {
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)

if (callback !== null) {
if (this.throwOnError && statusCode >= 400) {
this.runInAsyncScope(callback, null,
new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)
)
return
}

this.runInAsyncScope(callback, null, null, {
statusCode,
headers,
Expand Down
5 changes: 5 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,11 @@ class Parser {
// have been queued since then.
util.destroy(socket, new InformationalError('reset'))
return constants.ERROR.PAUSED
} else if (client[kPipelining] === 1) {
// We must wait a full event loop cycle to reuse this socket to make sure
// that non-spec compliant servers are not closing the connection even if they
// said they won't.
setImmediate(resume, client)
} else {
resume(client)
}
Expand Down
14 changes: 14 additions & 0 deletions lib/core/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ class BodyTimeoutError extends UndiciError {
}
}

class ResponseStatusCodeError extends UndiciError {
constructor (message, statusCode, headers) {
super(message)
Error.captureStackTrace(this, ResponseStatusCodeError)
this.name = 'ResponseStatusCodeError'
this.message = message || 'Response Status Code Error'
this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
this.status = statusCode
this.statusCode = statusCode
this.headers = headers
}
}

class InvalidArgumentError extends UndiciError {
constructor (message) {
super(message)
Expand Down Expand Up @@ -186,6 +199,7 @@ module.exports = {
BodyTimeoutError,
RequestContentLengthMismatchError,
ConnectTimeoutError,
ResponseStatusCodeError,
InvalidArgumentError,
InvalidReturnValueError,
RequestAbortedError,
Expand Down
10 changes: 7 additions & 3 deletions lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const {
InvalidArgumentError,
NotSupportedError
} = require('./errors')
const util = require('./util')
const assert = require('assert')
const util = require('./util')

const kHandler = Symbol('handler')

Expand Down Expand Up @@ -38,11 +38,13 @@ class Request {
method,
body,
headers,
query,
idempotent,
blocking,
upgrade,
headersTimeout,
bodyTimeout
bodyTimeout,
throwOnError
}, handler) {
if (typeof path !== 'string') {
throw new InvalidArgumentError('path must be a string')
Expand Down Expand Up @@ -70,6 +72,8 @@ class Request {

this.bodyTimeout = bodyTimeout

this.throwOnError = throwOnError === true

this.method = method

if (body == null) {
Expand Down Expand Up @@ -97,7 +101,7 @@ class Request {

this.upgrade = upgrade || null

this.path = path
this.path = query ? util.buildURL(path, query) : path

this.origin = origin

Expand Down
48 changes: 47 additions & 1 deletion lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,51 @@ function isBlobLike (object) {
)
}

function isObject (val) {
return val !== null && typeof val === 'object'
}

// this escapes all non-uri friendly characters
function encode (val) {
return encodeURIComponent(val)
}

// based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license)
function buildURL (url, queryParams) {
if (url.includes('?') || url.includes('#')) {
throw new Error('Query params cannot be passed when url already contains "?" or "#".')
}
if (!isObject(queryParams)) {
throw new Error('Query params must be an object')
}

const parts = []
for (let [key, val] of Object.entries(queryParams)) {
if (val === null || typeof val === 'undefined') {
continue
}

if (!Array.isArray(val)) {
val = [val]
}

for (const v of val) {
if (isObject(v)) {
throw new Error('Passing object as a query param is not supported, please serialize to string up-front')
}
parts.push(encode(key) + '=' + encode(v))
}
}

const serializedParams = parts.join('&')

if (serializedParams) {
url += '?' + serializedParams
}

return url
}

function parseURL (url) {
if (typeof url === 'string') {
url = new URL(url)
Expand Down Expand Up @@ -357,5 +402,6 @@ module.exports = {
isBuffer,
validateHandler,
getSocketInfo,
isFormDataLike
isFormDataLike,
buildURL
}
2 changes: 2 additions & 0 deletions lib/fetch/formdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { File, FileLike } = require('./file')
const { Blob } = require('buffer')

class FormData {
static name = 'FormData'

constructor (...args) {
if (args.length > 0 && !(args[0]?.constructor?.name === 'HTMLFormElement')) {
throw new TypeError(
Expand Down
18 changes: 3 additions & 15 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const {
coarsenedSharedCurrentTime,
createDeferredPromise,
isBlobLike,
CORBCheck,
sameOrigin,
isCancelled,
isAborted
Expand All @@ -52,7 +51,6 @@ const EE = require('events')
const { Readable, pipeline } = require('stream')
const { isErrored, isReadable } = require('../core/util')
const { dataURLProcessor } = require('./dataURL')
const { kIsMockActive } = require('../mock/mock-symbols')
const { TransformStream } = require('stream/web')

/** @type {import('buffer').resolveObjectURL} */
Expand Down Expand Up @@ -588,18 +586,8 @@ async function mainFetch (fetchParams, recursive = false) {
// 2. Set request’s response tainting to "opaque".
request.responseTainting = 'opaque'

// 3. Let noCorsResponse be the result of running scheme fetch given
// fetchParams.
const noCorsResponse = await schemeFetch(fetchParams)

// 4. If noCorsResponse is a filtered response or the CORB check with
// request and noCorsResponse returns allowed, then return noCorsResponse.
if (noCorsResponse.status === 0 || CORBCheck(request, noCorsResponse) === 'allowed') {
return noCorsResponse
}

// 5. Return a new response whose status is noCorsResponse’s status.
return makeResponse({ status: noCorsResponse.status })
// 3. Return the result of running scheme fetch given fetchParams.
return await schemeFetch(fetchParams)
}

// request’s current URL’s scheme is not an HTTP(S) scheme
Expand Down Expand Up @@ -1923,7 +1911,7 @@ async function httpNetworkFetch (
path: url.pathname + url.search,
origin: url.origin,
method: request.method,
body: fetchParams.controller.dispatcher[kIsMockActive] ? request.body && request.body.source : body,
body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body,
headers: [...request.headersList].flat(),
maxRedirections: 0,
bodyTimeout: 300_000,
Expand Down
Loading

0 comments on commit ce0abac

Please sign in to comment.