@@ -185,4 +185,4 @@ # Diagnostics Channel Support | ||
| // Handshake response details | ||
| console.log(handshakeResponse.status) // 101 for successful WebSocket upgrade | ||
| console.log(handshakeResponse.statusText) // 'Switching Protocols' | ||
| console.log(handshakeResponse.status) // 101 for HTTP/1.1, 200 for HTTP/2 extended CONNECT | ||
| console.log(handshakeResponse.statusText) // 'Switching Protocols' for HTTP/1.1, commonly 'OK' for HTTP/2 in Node.js | ||
| console.log(handshakeResponse.headers) // Object containing response headers | ||
@@ -194,11 +194,13 @@ }) | ||
| The `handshakeResponse` object contains the HTTP response that upgraded the connection to WebSocket: | ||
| The `handshakeResponse` object contains the HTTP response that established the WebSocket connection: | ||
| - `status` (number): The HTTP status code (101 for successful WebSocket upgrade) | ||
| - `statusText` (string): The HTTP status message ('Switching Protocols' for successful upgrade) | ||
| - `status` (number): The HTTP status code (`101` for HTTP/1.1 upgrade, `200` for HTTP/2 extended CONNECT) | ||
| - `statusText` (string): The HTTP status message (`'Switching Protocols'` for HTTP/1.1, commonly `'OK'` for HTTP/2 in Node.js) | ||
| - `headers` (object): The HTTP response headers from the server, including: | ||
| - `sec-websocket-accept` and other WebSocket-related headers | ||
| - `upgrade: 'websocket'` | ||
| - `connection: 'upgrade'` | ||
| - `sec-websocket-accept` and other WebSocket-related headers | ||
| The `upgrade` and `connection` headers are only present for HTTP/1.1 handshakes. | ||
| This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection. | ||
@@ -205,0 +207,0 @@ |
@@ -13,2 +13,10 @@ # Fetch | ||
| When you use `FormData` as a request body, keep `fetch` and `FormData` from the | ||
| same implementation. Use the built-in global `FormData` with the built-in | ||
| global `fetch()`, and use `undici`'s `FormData` with `undici.fetch()`. | ||
| If you want the installed `undici` package to provide the globals, call | ||
| [`install()`](/docs/api/GlobalInstallation.md) so `fetch`, `Headers`, | ||
| `Response`, `Request`, and `FormData` are installed together as a matching set. | ||
| ## Response | ||
@@ -15,0 +23,0 @@ |
@@ -46,2 +46,50 @@ # Global Installation | ||
| ## Using `FormData` with `fetch` | ||
| If you send a `FormData` body, use matching implementations for `fetch` and | ||
| `FormData`. | ||
| These two patterns are safe: | ||
| ```js | ||
| // Built-in globals from Node.js | ||
| const body = new FormData() | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| ```js | ||
| // Globals installed from the undici package | ||
| import { install } from 'undici' | ||
| install() | ||
| const body = new FormData() | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| After `install()`, `fetch`, `Headers`, `Response`, `Request`, and `FormData` | ||
| all come from the installed `undici` package, so they work as a matching set. | ||
| If you do not want to install globals, import both from `undici` instead: | ||
| ```js | ||
| import { fetch, FormData } from 'undici' | ||
| const body = new FormData() | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData` | ||
| with the built-in global `fetch()`. Keeping them paired avoids surprising | ||
| multipart behavior across Node.js and undici versions. | ||
| ## Use Cases | ||
@@ -48,0 +96,0 @@ |
@@ -22,2 +22,89 @@ # Undici Module vs. Node.js Built-in Fetch | ||
| ## Keep `fetch` and `FormData` from the same implementation | ||
| When you send a `FormData` body, keep `fetch` and `FormData` together from the | ||
| same implementation. | ||
| Use one of these patterns: | ||
| ### Built-in globals | ||
| ```js | ||
| const body = new FormData() | ||
| body.set('name', 'some') | ||
| body.set('someOtherProperty', '8000') | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| ### `undici` module imports | ||
| ```js | ||
| import { fetch, FormData } from 'undici' | ||
| const body = new FormData() | ||
| body.set('name', 'some') | ||
| body.set('someOtherProperty', '8000') | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| ### `undici.install()` globals | ||
| If you want the installed `undici` package to provide the globals, call | ||
| [`install()`](/docs/api/GlobalInstallation.md): | ||
| ```js | ||
| import { install } from 'undici' | ||
| install() | ||
| const body = new FormData() | ||
| body.set('name', 'some') | ||
| body.set('someOtherProperty', '8000') | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| `install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and | ||
| `FormData` implementations with undici's versions, and also installs undici's | ||
| `WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and `EventSource` | ||
| globals. | ||
| Avoid mixing implementations in the same request, for example: | ||
| ```js | ||
| import { fetch } from 'undici' | ||
| const body = new FormData() | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| ```js | ||
| import { FormData } from 'undici' | ||
| const body = new FormData() | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| Those combinations may behave differently across Node.js and undici versions. | ||
| Using matching pairs keeps multipart handling predictable. | ||
| ## When you do NOT need to install undici | ||
@@ -123,8 +210,8 @@ | ||
| Installing undici from npm does not replace the built-in globals. If you want | ||
| your installed version to override the global `fetch`, use | ||
| [`setGlobalDispatcher`](/docs/api/GlobalInstallation.md) or import `fetch` | ||
| your installed version to replace the global `fetch` and related classes, use | ||
| [`install()`](/docs/api/GlobalInstallation.md). Otherwise, import `fetch` | ||
| directly from `'undici'`: | ||
| ```js | ||
| import { fetch } from 'undici'; // uses your installed version, not the built-in | ||
| import { fetch } from 'undici' // uses your installed version, not the built-in | ||
| ``` | ||
@@ -131,0 +218,0 @@ |
@@ -180,6 +180,8 @@ 'use strict' | ||
| evt => { | ||
| const { | ||
| address: { address, port } | ||
| } = evt | ||
| debugLog('connection opened %s%s', address, port ? `:${port}` : '') | ||
| if (evt.address != null) { | ||
| const { address, port } = evt.address | ||
| debugLog('connection opened %s%s', address, port ? `:${port}` : '') | ||
| } else { | ||
| debugLog('connection opened') | ||
| } | ||
| }) | ||
@@ -186,0 +188,0 @@ |
+12
-4
@@ -415,9 +415,17 @@ 'use strict' | ||
| } else if (headerName === 'connection') { | ||
| const value = typeof val === 'string' ? val.toLowerCase() : null | ||
| if (value !== 'close' && value !== 'keep-alive') { | ||
| // Per RFC 7230 Section 6.1, Connection header can contain | ||
| // a comma-separated list of connection option tokens (header names) | ||
| const value = typeof val === 'string' ? val : null | ||
| if (value === null) { | ||
| throw new InvalidArgumentError('invalid connection header') | ||
| } | ||
| if (value === 'close') { | ||
| request.reset = true | ||
| for (const token of value.toLowerCase().split(',')) { | ||
| const trimmed = token.trim() | ||
| if (!isValidHTTPToken(trimmed)) { | ||
| throw new InvalidArgumentError('invalid connection header') | ||
| } | ||
| if (trimmed === 'close') { | ||
| request.reset = true | ||
| } | ||
| } | ||
@@ -424,0 +432,0 @@ } else if (headerName === 'expect') { |
+31
-11
@@ -443,15 +443,35 @@ 'use strict' | ||
| if (val) { | ||
| if (typeof val === 'string') { | ||
| val = [val] | ||
| obj[key] = val | ||
| if (val !== undefined) { | ||
| if (!Object.hasOwn(obj, key)) { | ||
| const headersValue = typeof headers[i + 1] === 'string' | ||
| ? headers[i + 1] | ||
| : Array.isArray(headers[i + 1]) | ||
| ? headers[i + 1].map(x => x.toString('latin1')) | ||
| : headers[i + 1].toString('latin1') | ||
| if (key === '__proto__') { | ||
| Object.defineProperty(obj, key, { | ||
| value: headersValue, | ||
| enumerable: true, | ||
| configurable: true, | ||
| writable: true | ||
| }) | ||
| } else { | ||
| obj[key] = headersValue | ||
| } | ||
| } else { | ||
| if (typeof val === 'string') { | ||
| val = [val] | ||
| obj[key] = val | ||
| } | ||
| val.push(headers[i + 1].toString('latin1')) | ||
| } | ||
| val.push(headers[i + 1].toString('latin1')) | ||
| } else { | ||
| const headersValue = headers[i + 1] | ||
| if (typeof headersValue === 'string') { | ||
| obj[key] = headersValue | ||
| } else { | ||
| obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('latin1')) : headersValue.toString('latin1') | ||
| } | ||
| const headersValue = typeof headers[i + 1] === 'string' | ||
| ? headers[i + 1] | ||
| : Array.isArray(headers[i + 1]) | ||
| ? headers[i + 1].map(x => x.toString('latin1')) | ||
| : headers[i + 1].toString('latin1') | ||
| obj[key] = headersValue | ||
| } | ||
@@ -458,0 +478,0 @@ } |
+56
-51
@@ -455,61 +455,66 @@ 'use strict' | ||
| client[kConnector]({ | ||
| host, | ||
| hostname, | ||
| protocol, | ||
| port, | ||
| servername: client[kServerName], | ||
| localAddress: client[kLocalAddress] | ||
| }, (err, socket) => { | ||
| if (err) { | ||
| handleConnectError(client, err, { host, hostname, protocol, port }) | ||
| client[kResume]() | ||
| return | ||
| } | ||
| try { | ||
| client[kConnector]({ | ||
| host, | ||
| hostname, | ||
| protocol, | ||
| port, | ||
| servername: client[kServerName], | ||
| localAddress: client[kLocalAddress] | ||
| }, (err, socket) => { | ||
| if (err) { | ||
| handleConnectError(client, err, { host, hostname, protocol, port }) | ||
| client[kResume]() | ||
| return | ||
| } | ||
| if (client.destroyed) { | ||
| util.destroy(socket.on('error', noop), new ClientDestroyedError()) | ||
| client[kResume]() | ||
| return | ||
| } | ||
| if (client.destroyed) { | ||
| util.destroy(socket.on('error', noop), new ClientDestroyedError()) | ||
| client[kResume]() | ||
| return | ||
| } | ||
| assert(socket) | ||
| assert(socket) | ||
| try { | ||
| client[kHTTPContext] = socket.alpnProtocol === 'h2' | ||
| ? connectH2(client, socket) | ||
| : connectH1(client, socket) | ||
| } catch (err) { | ||
| socket.destroy().on('error', noop) | ||
| handleConnectError(client, err, { host, hostname, protocol, port }) | ||
| client[kResume]() | ||
| return | ||
| } | ||
| try { | ||
| client[kHTTPContext] = socket.alpnProtocol === 'h2' | ||
| ? connectH2(client, socket) | ||
| : connectH1(client, socket) | ||
| } catch (err) { | ||
| socket.destroy().on('error', noop) | ||
| handleConnectError(client, err, { host, hostname, protocol, port }) | ||
| client[kResume]() | ||
| return | ||
| } | ||
| client[kConnecting] = false | ||
| client[kConnecting] = false | ||
| socket[kCounter] = 0 | ||
| socket[kMaxRequests] = client[kMaxRequests] | ||
| socket[kClient] = client | ||
| socket[kError] = null | ||
| socket[kCounter] = 0 | ||
| socket[kMaxRequests] = client[kMaxRequests] | ||
| socket[kClient] = client | ||
| socket[kError] = null | ||
| if (channels.connected.hasSubscribers) { | ||
| channels.connected.publish({ | ||
| connectParams: { | ||
| host, | ||
| hostname, | ||
| protocol, | ||
| port, | ||
| version: client[kHTTPContext]?.version, | ||
| servername: client[kServerName], | ||
| localAddress: client[kLocalAddress] | ||
| }, | ||
| connector: client[kConnector], | ||
| socket | ||
| }) | ||
| } | ||
| if (channels.connected.hasSubscribers) { | ||
| channels.connected.publish({ | ||
| connectParams: { | ||
| host, | ||
| hostname, | ||
| protocol, | ||
| port, | ||
| version: client[kHTTPContext]?.version, | ||
| servername: client[kServerName], | ||
| localAddress: client[kLocalAddress] | ||
| }, | ||
| connector: client[kConnector], | ||
| socket | ||
| }) | ||
| } | ||
| client.emit('connect', client[kUrl], [client]) | ||
| client.emit('connect', client[kUrl], [client]) | ||
| client[kResume]() | ||
| }) | ||
| } catch (err) { | ||
| handleConnectError(client, err, { host, hostname, protocol, port }) | ||
| client[kResume]() | ||
| }) | ||
| } | ||
| } | ||
@@ -516,0 +521,0 @@ |
@@ -138,3 +138,3 @@ 'use strict' | ||
| const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {} | ||
| if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) { | ||
| if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) { | ||
| return downstreamOnHeaders() | ||
@@ -344,4 +344,5 @@ } | ||
| * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives | ||
| * @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders] | ||
| */ | ||
| function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) { | ||
| function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) { | ||
| // Status code must be final and understood. | ||
@@ -377,7 +378,15 @@ if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) { | ||
| // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen | ||
| if (resHeaders.authorization) { | ||
| if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') { | ||
| if (reqHeaders?.authorization) { | ||
| if ( | ||
| !cacheControlDirectives.public && | ||
| !cacheControlDirectives['s-maxage'] && | ||
| !cacheControlDirectives['must-revalidate'] | ||
| ) { | ||
| return false | ||
| } | ||
| if (typeof reqHeaders.authorization !== 'string') { | ||
| return false | ||
| } | ||
| if ( | ||
@@ -384,0 +393,0 @@ Array.isArray(cacheControlDirectives['no-cache']) && |
@@ -30,3 +30,4 @@ 'use strict' | ||
| kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'), | ||
| kMockCallHistoryAddLog: Symbol('mock call history add log') | ||
| kMockCallHistoryAddLog: Symbol('mock call history add log'), | ||
| kTotalDispatchCount: Symbol('total dispatch count') | ||
| } |
@@ -9,3 +9,4 @@ 'use strict' | ||
| kOrigin, | ||
| kGetNetConnect | ||
| kGetNetConnect, | ||
| kTotalDispatchCount | ||
| } = require('./mock-symbols') | ||
@@ -210,2 +211,4 @@ const { serializePathWithQuery } = require('../core/util') | ||
| mockDispatches.push(newMockDispatch) | ||
| // Track total number of intercepts ever registered for better error messages | ||
| mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1 | ||
| return newMockDispatch | ||
@@ -406,4 +409,7 @@ } | ||
| const netConnect = agent[kGetNetConnect]() | ||
| const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length | ||
| const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length | ||
| const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined` | ||
| if (netConnect === false) { | ||
| throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) | ||
| throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`) | ||
| } | ||
@@ -413,3 +419,3 @@ if (checkNetConnect(netConnect, origin)) { | ||
| } else { | ||
| throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) | ||
| throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`) | ||
| } | ||
@@ -416,0 +422,0 @@ } else { |
@@ -28,2 +28,14 @@ 'use strict' | ||
| function getSocketAddress (socket) { | ||
| if (typeof socket?.address === 'function') { | ||
| return socket.address() | ||
| } | ||
| if (typeof socket?.session?.socket?.address === 'function') { | ||
| return socket.session.socket.address() | ||
| } | ||
| return null | ||
| } | ||
| /** | ||
@@ -495,3 +507,3 @@ * @typedef {object} Handler | ||
| channels.open.publish({ | ||
| address: response.socket.address(), | ||
| address: getSocketAddress(response.socket), | ||
| protocol: this.#protocol, | ||
@@ -498,0 +510,0 @@ extensions: this.#extensions, |
+2
-2
| { | ||
| "name": "undici", | ||
| "version": "7.24.5", | ||
| "version": "7.24.6", | ||
| "description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -132,3 +132,3 @@ "homepage": "https://undici.nodejs.org", | ||
| "tsd": "^0.33.0", | ||
| "typescript": "^5.6.2", | ||
| "typescript": "^6.0.2", | ||
| "ws": "^8.11.0" | ||
@@ -135,0 +135,0 @@ }, |
+56
-0
@@ -157,2 +157,53 @@ # undici | ||
| ### Keep `fetch` and `FormData` together | ||
| When you send a `FormData` body, keep `fetch` and `FormData` from the same | ||
| implementation. | ||
| Use one of these patterns: | ||
| ```js | ||
| // Built-in globals | ||
| const body = new FormData() | ||
| body.set('name', 'some') | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| ```js | ||
| // undici module imports | ||
| import { fetch, FormData } from 'undici' | ||
| const body = new FormData() | ||
| body.set('name', 'some') | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| If you want the installed `undici` package to provide the globals, call | ||
| `install()` first: | ||
| ```js | ||
| import { install } from 'undici' | ||
| install() | ||
| const body = new FormData() | ||
| body.set('name', 'some') | ||
| await fetch('https://example.com', { | ||
| method: 'POST', | ||
| body | ||
| }) | ||
| ``` | ||
| `install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and | ||
| `FormData` implementations with undici's versions, so they all match. | ||
| Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData` | ||
| with the built-in global `fetch()`. | ||
| ### Version Compatibility | ||
@@ -267,2 +318,7 @@ | ||
| When you call `install()`, these globals come from the same undici | ||
| implementation. For example, global `fetch` and global `FormData` will both be | ||
| undici's versions, which is the recommended setup if you want to use undici | ||
| through globals. | ||
| This is useful for: | ||
@@ -269,0 +325,0 @@ - Polyfilling environments that don't have fetch |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 9 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 9 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1608018
0.44%34041
0.17%742
8.16%75
1.35%