| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent bf9240a commit 2a4c826
28 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | [](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [](http://standardjs.com/) [](https://badge.fury.io/js/undici) [](https://codecov.io/gh/nodejs/undici) | |
| 4 | 4 | ||
| 5 | - A HTTP/1.1 client, written from scratch for Node.js. | ||
| 5 | + An HTTP/1.1 client, written from scratch for Node.js. | ||
| 6 | 6 | ||
| 7 | 7 | > Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici. | |
| 8 | 8 | It is also a Stranger Things reference. | |
@@ -65,7 +65,15 @@ for await (const data of body) { | |||
| 65 | 65 | console.log('trailers', trailers) | |
| 66 | 66 | ``` | |
| 67 | 67 | ||
| 68 | - Using [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). | ||
| 68 | + ## Body Mixins | ||
| 69 | + | ||
| 70 | + The `body` mixins are the most common way to format the request/response body. Mixins include: | ||
| 71 | + | ||
| 72 | + - [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata) | ||
| 73 | + - [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) | ||
| 74 | + - [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) | ||
| 75 | + | ||
| 76 | + Example usage: | ||
| 69 | 77 | ||
| 70 | 78 | ```js | |
| 71 | 79 | import { request } from 'undici' | |
@@ -83,6 +91,12 @@ console.log('data', await body.json()) | |||
| 83 | 91 | console.log('trailers', trailers) | |
| 84 | 92 | ``` | |
| 85 | 93 | ||
| 94 | + _Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._ | ||
| 95 | + | ||
| 96 | + Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format. | ||
| 97 | + | ||
| 98 | + For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). | ||
| 99 | + | ||
| 86 | 100 | ## Common API Methods | |
| 87 | 101 | ||
| 88 | 102 | This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site. | |
@@ -213,7 +227,7 @@ const data = { | |||
| 213 | 227 | ||
| 214 | 228 | #### `response.body` | |
| 215 | 229 | ||
| 216 | - Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html) which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`. | ||
| 230 | + Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`. | ||
| 217 | 231 | ||
| 218 | 232 | ```js | |
| 219 | 233 | import {fetch} from 'undici'; | |
@@ -228,7 +242,7 @@ Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v1 | |||
| 228 | 242 | ||
| 229 | 243 | #### Specification Compliance | |
| 230 | 244 | ||
| 231 | - This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) which Undici does | ||
| 245 | + This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does | ||
| 232 | 246 | not support or does not fully implement. | |
| 233 | 247 | ||
| 234 | 248 | ##### Garbage Collection | |
@@ -239,7 +253,7 @@ The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consumi | |||
| 239 | 253 | [garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body. | |
| 240 | 254 | ||
| 241 | 255 | Garbage collection in Node is less aggressive and deterministic | |
| 242 | - (due to the lack of clear idle periods that browser have through the rendering refresh rate) | ||
| 256 | + (due to the lack of clear idle periods that browsers have through the rendering refresh rate) | ||
| 243 | 257 | which means that leaving the release of connection resources to the garbage collector can lead | |
| 244 | 258 | to excessive connection usage, reduced performance (due to less connection re-use), and even | |
| 245 | 259 | stalls or deadlocks when running out of connections. | |
@@ -301,7 +315,7 @@ Returns: `Dispatcher` | |||
| 301 | 315 | ||
| 302 | 316 | ## Specification Compliance | |
| 303 | 317 | ||
| 304 | - This section documents parts of the HTTP/1.1 specification which Undici does | ||
| 318 | + This section documents parts of the HTTP/1.1 specification that Undici does | ||
| 305 | 319 | not support or does not fully implement. | |
| 306 | 320 | ||
| 307 | 321 | ### Expect | |
@@ -334,7 +348,7 @@ aborted. | |||
| 334 | 348 | ||
| 335 | 349 | ### Manual Redirect | |
| 336 | 350 | ||
| 337 | - Since it is not possible to manually follow an HTTP redirect on server-side, | ||
| 351 | + Since it is not possible to manually follow an HTTP redirect on the server-side, | ||
| 338 | 352 | Undici returns the actual response instead of an `opaqueredirect` filtered one | |
| 339 | 353 | when invoked with a `manual` redirect. This aligns `fetch()` with the other | |
| 340 | 354 | implementations in Deno and Cloudflare Workers. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -193,7 +193,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo | |||
| 193 | 193 | * **path** `string` | |
| 194 | 194 | * **method** `string` | |
| 195 | 195 | * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` | |
| 196 | - * **headers** `UndiciHeaders` (optional) - Default: `null` | ||
| 196 | + * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. | ||
| 197 | 197 | * **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. | |
| 198 | 198 | * **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. | |
| 199 | 199 | * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -445,3 +445,79 @@ mockAgent.disableNetConnect() | |||
| 445 | 445 | await request('http://example.com') | |
| 446 | 446 | // Will throw | |
| 447 | 447 | ``` | |
| 448 | + | ||
| 449 | + ### `MockAgent.pendingInterceptors()` | ||
| 450 | + | ||
| 451 | + This method returns any pending interceptors registered on a mock agent. A pending interceptor meets one of the following criteria: | ||
| 452 | + | ||
| 453 | + - Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked; | ||
| 454 | + - Is persistent (i.e., registered with `.persist()`) and has not been invoked; | ||
| 455 | + - Is registered with `.times(<number>)` and has not been invoked `<number>` of times. | ||
| 456 | + | ||
| 457 | + Returns: `PendingInterceptor[]` (where `PendingInterceptor` is a `MockDispatch` with an additional `origin: string`) | ||
| 458 | + | ||
| 459 | + #### Example - List all pending inteceptors | ||
| 460 | + | ||
| 461 | + ```js | ||
| 462 | + const agent = new MockAgent() | ||
| 463 | + agent.disableNetConnect() | ||
| 464 | + | ||
| 465 | + agent | ||
| 466 | + .get('https://example.com') | ||
| 467 | + .intercept({ method: 'GET', path: '/' }) | ||
| 468 | + .reply(200, '') | ||
| 469 | + | ||
| 470 | + const pendingInterceptors = agent.pendingInterceptors() | ||
| 471 | + // Returns [ | ||
| 472 | + // { | ||
| 473 | + // timesInvoked: 0, | ||
| 474 | + // times: 1, | ||
| 475 | + // persist: false, | ||
| 476 | + // consumed: false, | ||
| 477 | + // pending: true, | ||
| 478 | + // path: '/', | ||
| 479 | + // method: 'GET', | ||
| 480 | + // body: undefined, | ||
| 481 | + // headers: undefined, | ||
| 482 | + // data: { | ||
| 483 | + // error: null, | ||
| 484 | + // statusCode: 200, | ||
| 485 | + // data: '', | ||
| 486 | + // headers: {}, | ||
| 487 | + // trailers: {} | ||
| 488 | + // }, | ||
| 489 | + // origin: 'https://example.com' | ||
| 490 | + // } | ||
| 491 | + // ] | ||
| 492 | + ``` | ||
| 493 | + | ||
| 494 | + ### `MockAgent.assertNoPendingInterceptors([options])` | ||
| 495 | + | ||
| 496 | + This method throws if the mock agent has any pending interceptors. A pending interceptor meets one of the following criteria: | ||
| 497 | + | ||
| 498 | + - Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked; | ||
| 499 | + - Is persistent (i.e., registered with `.persist()`) and has not been invoked; | ||
| 500 | + - Is registered with `.times(<number>)` and has not been invoked `<number>` of times. | ||
| 501 | + | ||
| 502 | + #### Example - Check that there are no pending interceptors | ||
| 503 | + | ||
| 504 | + ```js | ||
| 505 | + const agent = new MockAgent() | ||
| 506 | + agent.disableNetConnect() | ||
| 507 | + | ||
| 508 | + agent | ||
| 509 | + .get('https://example.com') | ||
| 510 | + .intercept({ method: 'GET', path: '/' }) | ||
| 511 | + .reply(200, '') | ||
| 512 | + | ||
| 513 | + agent.assertNoPendingInterceptors() | ||
| 514 | + // Throws an UndiciError with the following message: | ||
| 515 | + // | ||
| 516 | + // 1 interceptor is pending: | ||
| 517 | + // | ||
| 518 | + // ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ | ||
| 519 | + // │ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │ | ||
| 520 | + // ├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤ | ||
| 521 | + // │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │ | ||
| 522 | + // └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ | ||
| 523 | + ``` | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,17 +5,20 @@ Undici have its own mocking [utility](../api/MockAgent.md). It allow us to inter | |||
| 5 | 5 | Example: | |
| 6 | 6 | ||
| 7 | 7 | ```js | |
| 8 | - // index.mjs | ||
| 8 | + // bank.mjs | ||
| 9 | 9 | import { request } from 'undici' | |
| 10 | 10 | ||
| 11 | - export async function bankTransfer(recepient, ammount) { | ||
| 12 | - const { body } = await request('http://localhost:3000/bank-transfer', | ||
| 11 | + export async function bankTransfer(recepient, amount) { | ||
| 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 | - body: JSON.stringify({ recepient }) | ||
| 18 | + body: JSON.stringify({ | ||
| 19 | + recepient, | ||
| 20 | + amount | ||
| 21 | + }) | ||
| 19 | 22 | } | |
| 20 | 23 | ) | |
| 21 | 24 | return await body.json() | |
@@ -28,7 +31,7 @@ And this is what the test file looks like: | |||
| 28 | 31 | // index.test.mjs | |
| 29 | 32 | import { strict as assert } from 'assert' | |
| 30 | 33 | import { MockAgent, setGlobalDispatcher, } from 'undici' | |
| 31 | - import { bankTransfer } from './undici.mjs' | ||
| 34 | + import { bankTransfer } from './bank.mjs' | ||
| 32 | 35 | ||
| 33 | 36 | const mockAgent = new MockAgent(); | |
| 34 | 37 | ||
@@ -46,7 +49,7 @@ mockPool.intercept({ | |||
| 46 | 49 | }, | |
| 47 | 50 | body: JSON.stringify({ | |
| 48 | 51 | recepient: '1234567890', | |
| 49 | - ammount: '100' | ||
| 52 | + amount: '100' | ||
| 50 | 53 | }) | |
| 51 | 54 | }).reply(200, { | |
| 52 | 55 | message: 'transaction processed' | |
@@ -94,7 +97,7 @@ mockPool.intercept({ | |||
| 94 | 97 | ||
| 95 | 98 | const badRequest = await bankTransfer('1234567890', '100') | |
| 96 | 99 | // Will throw an error | |
| 97 | - // MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer': | ||
| 100 | + // MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer': | ||
| 98 | 101 | // subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled) | |
| 99 | 102 | ``` | |
| 100 | 103 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,14 @@ | |||
| 1 | + 'use strict' | ||
| 2 | + | ||
| 3 | + const Agent = require('./lib/agent') | ||
| 4 | + | ||
| 5 | + const globalDispatcher = new Agent() | ||
| 6 | + | ||
| 7 | + const fetchImpl = require('./lib/fetch') | ||
| 8 | + module.exports.fetch = async function fetch (resource) { | ||
| 9 | + return fetchImpl.apply(globalDispatcher, arguments) | ||
| 10 | + } | ||
| 11 | + module.exports.FormData = require('./lib/fetch/formdata').FormData | ||
| 12 | + module.exports.Headers = require('./lib/fetch/headers').Headers | ||
| 13 | + module.exports.Response = require('./lib/fetch/response').Response | ||
| 14 | + module.exports.Request = require('./lib/fetch/request').Request | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -16,6 +16,7 @@ import { request, pipeline, stream, connect, upgrade } from './types/api' | |||
| 16 | 16 | export * from './types/fetch' | |
| 17 | 17 | export * from './types/file' | |
| 18 | 18 | export * from './types/formdata' | |
| 19 | + export { Interceptable } from './types/mock-interceptor' | ||
| 19 | 20 | ||
| 20 | 21 | export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent } | |
| 21 | 22 | export default Undici | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -88,14 +88,16 @@ class RequestHandler extends AsyncResource { | |||
| 88 | 88 | this.res = body | |
| 89 | 89 | const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) | |
| 90 | 90 | ||
| 91 | - this.runInAsyncScope(callback, null, null, { | ||
| 92 | - statusCode, | ||
| 93 | - headers, | ||
| 94 | - trailers: this.trailers, | ||
| 95 | - opaque, | ||
| 96 | - body, | ||
| 97 | - context | ||
| 98 | - }) | ||
| 91 | + if (callback !== null) { | ||
| 92 | + this.runInAsyncScope(callback, null, null, { | ||
| 93 | + statusCode, | ||
| 94 | + headers, | ||
| 95 | + trailers: this.trailers, | ||
| 96 | + opaque, | ||
| 97 | + body, | ||
| 98 | + context | ||
| 99 | + }) | ||
| 100 | + } | ||
| 99 | 101 | } | |
| 100 | 102 | ||
| 101 | 103 | onData (chunk) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,6 +11,12 @@ const kHandler = Symbol('handler') | |||
| 11 | 11 | ||
| 12 | 12 | const channels = {} | |
| 13 | 13 | ||
| 14 | + let extractBody | ||
| 15 | + | ||
| 16 | + const nodeVersion = process.versions.node.split('.') | ||
| 17 | + const nodeMajor = Number(nodeVersion[0]) | ||
| 18 | + const nodeMinor = Number(nodeVersion[1]) | ||
| 19 | + | ||
| 14 | 20 | try { | |
| 15 | 21 | const diagnosticsChannel = require('diagnostics_channel') | |
| 16 | 22 | channels.create = diagnosticsChannel.channel('undici:request:create') | |
@@ -79,7 +85,7 @@ class Request { | |||
| 79 | 85 | this.body = body.byteLength ? body : null | |
| 80 | 86 | } else if (typeof body === 'string') { | |
| 81 | 87 | this.body = body.length ? Buffer.from(body) : null | |
| 82 | - } else if (util.isIterable(body) || util.isBlobLike(body)) { | ||
| 88 | + } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { | ||
| 83 | 89 | this.body = body | |
| 84 | 90 | } else { | |
| 85 | 91 | throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') | |
@@ -126,7 +132,22 @@ class Request { | |||
| 126 | 132 | throw new InvalidArgumentError('headers must be an object or an array') | |
| 127 | 133 | } | |
| 128 | 134 | ||
| 129 | - if (util.isBlobLike(body) && this.contentType == null && body.type) { | ||
| 135 | + if (util.isFormDataLike(this.body)) { | ||
| 136 | + if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 5)) { | ||
| 137 | + throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.5 and newer.') | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + if (!extractBody) { | ||
| 141 | + extractBody = require('../fetch/body.js').extractBody | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + const [bodyStream, contentType] = extractBody(body) | ||
| 145 | + if (this.contentType == null) { | ||
| 146 | + this.contentType = contentType | ||
| 147 | + this.headers += `content-type: ${contentType}\r\n` | ||
| 148 | + } | ||
| 149 | + this.body = bodyStream.stream | ||
| 150 | + } else if (util.isBlobLike(body) && this.contentType == null && body.type) { | ||
| 130 | 151 | this.contentType = body.type | |
| 131 | 152 | this.headers += `content-type: ${body.type}\r\n` | |
| 132 | 153 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -324,6 +324,10 @@ function ReadableStreamFrom (iterable) { | |||
| 324 | 324 | ) | |
| 325 | 325 | } | |
| 326 | 326 | ||
| 327 | + function isFormDataLike (chunk) { | ||
| 328 | + return chunk && chunk.constructor && chunk.constructor.name === 'FormData' | ||
| 329 | + } | ||
| 330 | + | ||
| 327 | 331 | const kEnumerableProperty = Object.create(null) | |
| 328 | 332 | kEnumerableProperty.enumerable = true | |
| 329 | 333 | ||
@@ -352,5 +356,6 @@ module.exports = { | |||
| 352 | 356 | ReadableStreamFrom, | |
| 353 | 357 | isBuffer, | |
| 354 | 358 | validateHandler, | |
| 355 | - getSocketInfo | ||
| 359 | + getSocketInfo, | ||
| 360 | + isFormDataLike | ||
| 356 | 361 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -71,7 +71,7 @@ function extractBody (object, keepalive = false) { | |||
| 71 | 71 | ||
| 72 | 72 | // Set source to a copy of the bytes held by object. | |
| 73 | 73 | source = new Uint8Array(object) | |
| 74 | - } else if (object instanceof FormData) { | ||
| 74 | + } else if (util.isFormDataLike(object)) { | ||
| 75 | 75 | const boundary = '----formdata-undici-' + Math.random() | |
| 76 | 76 | const prefix = `--${boundary}\r\nContent-Disposition: form-data` | |
| 77 | 77 | ||
@@ -348,7 +348,7 @@ const properties = { | |||
| 348 | 348 | bodyUsed: { | |
| 349 | 349 | enumerable: true, | |
| 350 | 350 | get () { | |
| 351 | - return this[kState].body && util.isDisturbed(this[kState].body.stream) | ||
| 351 | + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) | ||
| 352 | 352 | } | |
| 353 | 353 | } | |
| 354 | 354 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments