| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 1b6d294 commit e404ac7
15 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,22 +1,22 @@ | |||
| 1 | 1 | # Mocking Request | |
| 2 | 2 | ||
| 3 | - Undici have its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP request and return mocked value instead. It can be useful for testing purposes. | ||
| 3 | + Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes. | ||
| 4 | 4 | ||
| 5 | 5 | Example: | |
| 6 | 6 | ||
| 7 | 7 | ```js | |
| 8 | 8 | // bank.mjs | |
| 9 | 9 | import { request } from 'undici' | |
| 10 | 10 | ||
| 11 | - export async function bankTransfer(recepient, amount) { | ||
| 11 | + export async function bankTransfer(recipient, amount) { | ||
| 12 | 12 | const { body } = await request('http://localhost:3000/bank-transfer', | |
| 13 | 13 | { | |
| 14 | 14 | method: 'POST', | |
| 15 | 15 | headers: { | |
| 16 | 16 | 'X-TOKEN-SECRET': 'SuperSecretToken', | |
| 17 | 17 | }, | |
| 18 | 18 | body: JSON.stringify({ | |
| 19 | - recepient, | ||
| 19 | + recipient, | ||
| 20 | 20 | amount | |
| 21 | 21 | }) | |
| 22 | 22 | } | |
@@ -48,7 +48,7 @@ mockPool.intercept({ | |||
| 48 | 48 | 'X-TOKEN-SECRET': 'SuperSecretToken', | |
| 49 | 49 | }, | |
| 50 | 50 | body: JSON.stringify({ | |
| 51 | - recepient: '1234567890', | ||
| 51 | + recipient: '1234567890', | ||
| 52 | 52 | amount: '100' | |
| 53 | 53 | }) | |
| 54 | 54 | }).reply(200, { | |
@@ -77,7 +77,7 @@ Explore other MockAgent functionality [here](../api/MockAgent.md) | |||
| 77 | 77 | ||
| 78 | 78 | ## Debug Mock Value | |
| 79 | 79 | ||
| 80 | - When the interceptor we wrote are not the same undici will automatically call real HTTP request. To debug our mock value use `mockAgent.disableNetConnect()` | ||
| 80 | + When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`: | ||
| 81 | 81 | ||
| 82 | 82 | ```js | |
| 83 | 83 | const mockAgent = new MockAgent(); | |
@@ -89,7 +89,7 @@ mockAgent.disableNetConnect() | |||
| 89 | 89 | const mockPool = mockAgent.get('http://localhost:3000'); | |
| 90 | 90 | ||
| 91 | 91 | mockPool.intercept({ | |
| 92 | - path: '/bank-tanfer', | ||
| 92 | + path: '/bank-transfer', | ||
| 93 | 93 | method: 'POST', | |
| 94 | 94 | }).reply(200, { | |
| 95 | 95 | message: 'transaction processed' | |
@@ -103,7 +103,7 @@ const badRequest = await bankTransfer('1234567890', '100') | |||
| 103 | 103 | ||
| 104 | 104 | ## Reply with data based on request | |
| 105 | 105 | ||
| 106 | - If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply` | ||
| 106 | + If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`: | ||
| 107 | 107 | ||
| 108 | 108 | ```js | |
| 109 | 109 | mockPool.intercept({ | |
@@ -113,7 +113,7 @@ mockPool.intercept({ | |||
| 113 | 113 | 'X-TOKEN-SECRET': 'SuperSecretToken', | |
| 114 | 114 | }, | |
| 115 | 115 | body: JSON.stringify({ | |
| 116 | - recepient: '1234567890', | ||
| 116 | + recipient: '1234567890', | ||
| 117 | 117 | amount: '100' | |
| 118 | 118 | }) | |
| 119 | 119 | }).reply(200, (opts) => { | |
@@ -129,7 +129,7 @@ in this case opts will be | |||
| 129 | 129 | { | |
| 130 | 130 | method: 'POST', | |
| 131 | 131 | headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' }, | |
| 132 | - body: '{"recepient":"1234567890","amount":"100"}', | ||
| 132 | + body: '{"recipient":"1234567890","amount":"100"}', | ||
| 133 | 133 | origin: 'http://localhost:3000', | |
| 134 | 134 | path: '/bank-transfer' | |
| 135 | 135 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -75,14 +75,12 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { | |||
| 75 | 75 | }) | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - const timeoutId = timeout | ||
| 79 | - ? setTimeout(onConnectTimeout, timeout, socket) | ||
| 80 | - : null | ||
| 78 | + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) | ||
| 81 | 79 | ||
| 82 | 80 | socket | |
| 83 | 81 | .setNoDelay(true) | |
| 84 | 82 | .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { | |
| 85 | - clearTimeout(timeoutId) | ||
| 83 | + cancelTimeout() | ||
| 86 | 84 | ||
| 87 | 85 | if (callback) { | |
| 88 | 86 | const cb = callback | |
@@ -91,7 +89,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { | |||
| 91 | 89 | } | |
| 92 | 90 | }) | |
| 93 | 91 | .on('error', function (err) { | |
| 94 | - clearTimeout(timeoutId) | ||
| 92 | + cancelTimeout() | ||
| 95 | 93 | ||
| 96 | 94 | if (callback) { | |
| 97 | 95 | const cb = callback | |
@@ -104,6 +102,31 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { | |||
| 104 | 102 | } | |
| 105 | 103 | } | |
| 106 | 104 | ||
| 105 | + function setupTimeout (onConnectTimeout, timeout) { | ||
| 106 | + if (!timeout) { | ||
| 107 | + return () => {} | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + let s1 = null | ||
| 111 | + let s2 = null | ||
| 112 | + const timeoutId = setTimeout(() => { | ||
| 113 | + // setImmediate is added to make sure that we priotorise socket error events over timeouts | ||
| 114 | + s1 = setImmediate(() => { | ||
| 115 | + if (process.platform === 'win32') { | ||
| 116 | + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic | ||
| 117 | + s2 = setImmediate(() => onConnectTimeout()) | ||
| 118 | + } else { | ||
| 119 | + onConnectTimeout() | ||
| 120 | + } | ||
| 121 | + }) | ||
| 122 | + }, timeout) | ||
| 123 | + return () => { | ||
| 124 | + clearTimeout(timeoutId) | ||
| 125 | + clearImmediate(s1) | ||
| 126 | + clearImmediate(s2) | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | + | ||
| 107 | 130 | function onConnectTimeout (socket) { | |
| 108 | 131 | util.destroy(socket, new ConnectTimeoutError()) | |
| 109 | 132 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -404,7 +404,18 @@ function bodyMixinMethods (instance) { | |||
| 404 | 404 | // 1. Let entries be the result of parsing bytes. | |
| 405 | 405 | let entries | |
| 406 | 406 | try { | |
| 407 | - entries = new URLSearchParams(await this.text()) | ||
| 407 | + let text = '' | ||
| 408 | + // application/x-www-form-urlencoded parser will keep the BOM. | ||
| 409 | + // https://url.spec.whatwg.org/#concept-urlencoded-parser | ||
| 410 | + const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) | ||
| 411 | + for await (const chunk of consumeBody(this[kState].body)) { | ||
| 412 | + if (!isUint8Array(chunk)) { | ||
| 413 | + throw new TypeError('Expected Uint8Array chunk') | ||
| 414 | + } | ||
| 415 | + text += textDecoder.decode(chunk, { stream: true }) | ||
| 416 | + } | ||
| 417 | + text += textDecoder.decode() | ||
| 418 | + entries = new URLSearchParams(text) | ||
| 408 | 419 | } catch (err) { | |
| 409 | 420 | // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. | |
| 410 | 421 | // 2. If entries is failure, then throw a TypeError. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -255,7 +255,7 @@ function percentDecode (input) { | |||
| 255 | 255 | } | |
| 256 | 256 | ||
| 257 | 257 | // 3. Return output. | |
| 258 | - return Uint8Array.of(...output) | ||
| 258 | + return Uint8Array.from(output) | ||
| 259 | 259 | } | |
| 260 | 260 | ||
| 261 | 261 | // https://mimesniff.spec.whatwg.org/#parse-a-mime-type | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,7 +33,8 @@ const { | |||
| 33 | 33 | isBlobLike, | |
| 34 | 34 | sameOrigin, | |
| 35 | 35 | isCancelled, | |
| 36 | - isAborted | ||
| 36 | + isAborted, | ||
| 37 | + isErrorLike | ||
| 37 | 38 | } = require('./util') | |
| 38 | 39 | const { kState, kHeaders, kGuard, kRealm } = require('./symbols') | |
| 39 | 40 | const assert = require('assert') | |
@@ -1854,7 +1855,7 @@ async function httpNetworkFetch ( | |||
| 1854 | 1855 | timingInfo.decodedBodySize += bytes?.byteLength ?? 0 | |
| 1855 | 1856 | ||
| 1856 | 1857 | // 6. If bytes is failure, then terminate fetchParams’s controller. | |
| 1857 | - if (bytes instanceof Error) { | ||
| 1858 | + if (isErrorLike(bytes)) { | ||
| 1858 | 1859 | fetchParams.controller.terminate(bytes) | |
| 1859 | 1860 | return | |
| 1860 | 1861 | } | |
@@ -1894,7 +1895,7 @@ async function httpNetworkFetch ( | |||
| 1894 | 1895 | // 3. Otherwise, if stream is readable, error stream with a TypeError. | |
| 1895 | 1896 | if (isReadable(stream)) { | |
| 1896 | 1897 | fetchParams.controller.controller.error(new TypeError('terminated', { | |
| 1897 | - cause: reason instanceof Error ? reason : undefined | ||
| 1898 | + cause: isErrorLike(reason) ? reason : undefined | ||
| 1898 | 1899 | })) | |
| 1899 | 1900 | } | |
| 1900 | 1901 | } | |
@@ -1942,14 +1943,17 @@ async function httpNetworkFetch ( | |||
| 1942 | 1943 | } | |
| 1943 | 1944 | ||
| 1944 | 1945 | let codings = [] | |
| 1946 | + let location = '' | ||
| 1945 | 1947 | ||
| 1946 | 1948 | const headers = new Headers() | |
| 1947 | 1949 | for (let n = 0; n < headersList.length; n += 2) { | |
| 1948 | - const key = headersList[n + 0].toString() | ||
| 1949 | - const val = headersList[n + 1].toString() | ||
| 1950 | + const key = headersList[n + 0].toString('latin1') | ||
| 1951 | + const val = headersList[n + 1].toString('latin1') | ||
| 1950 | 1952 | ||
| 1951 | 1953 | if (key.toLowerCase() === 'content-encoding') { | |
| 1952 | 1954 | codings = val.split(',').map((x) => x.trim()) | |
| 1955 | + } else if (key.toLowerCase() === 'location') { | ||
| 1956 | + location = val | ||
| 1953 | 1957 | } | |
| 1954 | 1958 | ||
| 1955 | 1959 | headers.append(key, val) | |
@@ -1960,7 +1964,7 @@ async function httpNetworkFetch ( | |||
| 1960 | 1964 | const decoders = [] | |
| 1961 | 1965 | ||
| 1962 | 1966 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding | |
| 1963 | - if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) { | ||
| 1967 | + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !(request.redirect === 'follow' && location)) { | ||
| 1964 | 1968 | for (const coding of codings) { | |
| 1965 | 1969 | if (/(x-)?gzip/.test(coding)) { | |
| 1966 | 1970 | decoders.push(zlib.createGunzip()) | |
@@ -1980,7 +1984,7 @@ async function httpNetworkFetch ( | |||
| 1980 | 1984 | statusText, | |
| 1981 | 1985 | headersList: headers[kHeadersList], | |
| 1982 | 1986 | body: decoders.length | |
| 1983 | - ? pipeline(this.body, ...decoders, () => {}) | ||
| 1987 | + ? pipeline(this.body, ...decoders, () => { }) | ||
| 1984 | 1988 | : this.body.on('error', () => {}) | |
| 1985 | 1989 | }) | |
| 1986 | 1990 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -367,9 +367,9 @@ class Request { | |||
| 367 | 367 | } | |
| 368 | 368 | ||
| 369 | 369 | if (signal.aborted) { | |
| 370 | - ac.abort() | ||
| 370 | + ac.abort(signal.reason) | ||
| 371 | 371 | } else { | |
| 372 | - const abort = () => ac.abort() | ||
| 372 | + const abort = () => ac.abort(signal.reason) | ||
| 373 | 373 | signal.addEventListener('abort', abort, { once: true }) | |
| 374 | 374 | requestFinalizer.register(this, { signal, abort }) | |
| 375 | 375 | } | |
@@ -726,12 +726,12 @@ class Request { | |||
| 726 | 726 | // 4. Make clonedRequestObject’s signal follow this’s signal. | |
| 727 | 727 | const ac = new AbortController() | |
| 728 | 728 | if (this.signal.aborted) { | |
| 729 | - ac.abort() | ||
| 729 | + ac.abort(this.signal.reason) | ||
| 730 | 730 | } else { | |
| 731 | 731 | this.signal.addEventListener( | |
| 732 | 732 | 'abort', | |
| 733 | - function () { | ||
| 734 | - ac.abort() | ||
| 733 | + () => { | ||
| 734 | + ac.abort(this.signal.reason) | ||
| 735 | 735 | }, | |
| 736 | 736 | { once: true } | |
| 737 | 737 | ) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,7 +10,8 @@ const { | |||
| 10 | 10 | isCancelled, | |
| 11 | 11 | isAborted, | |
| 12 | 12 | isBlobLike, | |
| 13 | - serializeJavascriptValueToJSONString | ||
| 13 | + serializeJavascriptValueToJSONString, | ||
| 14 | + isErrorLike | ||
| 14 | 15 | } = require('./util') | |
| 15 | 16 | const { | |
| 16 | 17 | redirectStatus, | |
@@ -347,15 +348,15 @@ function makeResponse (init) { | |||
| 347 | 348 | } | |
| 348 | 349 | ||
| 349 | 350 | function makeNetworkError (reason) { | |
| 351 | + const isError = isErrorLike(reason) | ||
| 350 | 352 | return makeResponse({ | |
| 351 | 353 | type: 'error', | |
| 352 | 354 | status: 0, | |
| 353 | - error: | ||
| 354 | - reason instanceof Error | ||
| 355 | - ? reason | ||
| 356 | - : new Error(reason ? String(reason) : reason, { | ||
| 357 | - cause: reason instanceof Error ? reason : undefined | ||
| 358 | - }), | ||
| 355 | + error: isError | ||
| 356 | + ? reason | ||
| 357 | + : new Error(reason ? String(reason) : reason, { | ||
| 358 | + cause: isError ? reason : undefined | ||
| 359 | + }), | ||
| 359 | 360 | aborted: reason && reason.name === 'AbortError' | |
| 360 | 361 | }) | |
| 361 | 362 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -82,6 +82,13 @@ function isFileLike (object) { | |||
| 82 | 82 | ) | |
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | + function isErrorLike (object) { | ||
| 86 | + return object instanceof Error || ( | ||
| 87 | + object?.constructor?.name === 'Error' || | ||
| 88 | + object?.constructor?.name === 'DOMException' | ||
| 89 | + ) | ||
| 90 | + } | ||
| 91 | + | ||
| 85 | 92 | // Check whether |statusText| is a ByteString and | |
| 86 | 93 | // matches the Reason-Phrase token production. | |
| 87 | 94 | // RFC 2616: https://tools.ietf.org/html/rfc2616 | |
@@ -469,5 +476,6 @@ module.exports = { | |||
| 469 | 476 | makeIterator, | |
| 470 | 477 | isValidHeaderName, | |
| 471 | 478 | isValidHeaderValue, | |
| 472 | - hasOwn | ||
| 479 | + hasOwn, | ||
| 480 | + isErrorLike | ||
| 473 | 481 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -388,8 +388,9 @@ webidl.converters.DOMString = function (V, opts = {}) { | |||
| 388 | 388 | return String(V) | |
| 389 | 389 | } | |
| 390 | 390 | ||
| 391 | + // Check for 0 or more characters outside of the latin1 range. | ||
| 391 | 392 | // eslint-disable-next-line no-control-regex | |
| 392 | - const isNotLatin1 = /[^\u0000-\u00ff]/ | ||
| 393 | + const isLatin1 = /^[\u0000-\u00ff]{0,}$/ | ||
| 393 | 394 | ||
| 394 | 395 | // https://webidl.spec.whatwg.org/#es-ByteString | |
| 395 | 396 | webidl.converters.ByteString = function (V) { | |
@@ -399,7 +400,7 @@ webidl.converters.ByteString = function (V) { | |||
| 399 | 400 | ||
| 400 | 401 | // 2. If the value of any element of x is greater than | |
| 401 | 402 | // 255, then throw a TypeError. | |
| 402 | - if (isNotLatin1.test(x)) { | ||
| 403 | + if (!isLatin1.test(x)) { | ||
| 403 | 404 | throw new TypeError('Argument is not a ByteString') | |
| 404 | 405 | } | |
| 405 | 406 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,7 +38,7 @@ function lowerCaseEntries (headers) { | |||
| 38 | 38 | function getHeaderByName (headers, key) { | |
| 39 | 39 | if (Array.isArray(headers)) { | |
| 40 | 40 | for (let i = 0; i < headers.length; i += 2) { | |
| 41 | - if (headers[i] === key) { | ||
| 41 | + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { | ||
| 42 | 42 | return headers[i + 1] | |
| 43 | 43 | } | |
| 44 | 44 | } | |
@@ -47,19 +47,24 @@ function getHeaderByName (headers, key) { | |||
| 47 | 47 | } else if (typeof headers.get === 'function') { | |
| 48 | 48 | return headers.get(key) | |
| 49 | 49 | } else { | |
| 50 | - return headers[key] | ||
| 50 | + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] | ||
| 51 | 51 | } | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | + /** @param {string[]} headers */ | ||
| 55 | + function buildHeadersFromArray (headers) { // fetch HeadersList | ||
| 56 | + const clone = headers.slice() | ||
| 57 | + const entries = [] | ||
| 58 | + for (let index = 0; index < clone.length; index += 2) { | ||
| 59 | + entries.push([clone[index], clone[index + 1]]) | ||
| 60 | + } | ||
| 61 | + return Object.fromEntries(entries) | ||
| 62 | + } | ||
| 63 | + | ||
| 54 | 64 | function matchHeaders (mockDispatch, headers) { | |
| 55 | 65 | if (typeof mockDispatch.headers === 'function') { | |
| 56 | 66 | if (Array.isArray(headers)) { // fetch HeadersList | |
| 57 | - const clone = headers.slice() | ||
| 58 | - const entries = [] | ||
| 59 | - for (let index = 0; index < clone.length; index += 2) { | ||
| 60 | - entries.push([clone[index], clone[index + 1]]) | ||
| 61 | - } | ||
| 62 | - headers = Object.fromEntries(entries) | ||
| 67 | + headers = buildHeadersFromArray(headers) | ||
| 63 | 68 | } | |
| 64 | 69 | return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) | |
| 65 | 70 | } | |
@@ -284,7 +289,13 @@ function mockDispatch (opts, handler) { | |||
| 284 | 289 | } | |
| 285 | 290 | ||
| 286 | 291 | function handleReply (mockDispatches) { | |
| 287 | - const responseData = getResponseData(typeof data === 'function' ? data(opts) : data) | ||
| 292 | + // fetch's HeadersList is a 1D string array | ||
| 293 | + const optsHeaders = Array.isArray(opts.headers) | ||
| 294 | + ? buildHeadersFromArray(opts.headers) | ||
| 295 | + : opts.headers | ||
| 296 | + const responseData = getResponseData( | ||
| 297 | + typeof data === 'function' ? data({ ...opts, headers: optsHeaders }) : data | ||
| 298 | + ) | ||
| 288 | 299 | const responseHeaders = generateKeyValues(headers) | |
| 289 | 300 | const responseTrailers = generateKeyValues(trailers) | |
| 290 | 301 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments