undici
Advanced tools
Comparing version 6.16.1 to 6.17.0
@@ -955,2 +955,34 @@ # Dispatcher | ||
##### `dump` | ||
The `dump` interceptor enables you to dump the response body from a request upon a given limit. | ||
**Options** | ||
- `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the request's body exceeds this value then the connection will be closed. Default: `1048576`. | ||
> The `Dispatcher#options` also gets extended with the options `dumpMaxSize`, `abortOnDumped`, and `waitForTrailers` which can be used to configure the interceptor at a request-per-request basis. | ||
**Example - Basic Dump Interceptor** | ||
```js | ||
const { Client, interceptors } = require("undici"); | ||
const { dump } = interceptors; | ||
const client = new Client("http://example.com").compose( | ||
dump({ | ||
maxSize: 1024, | ||
}) | ||
); | ||
// or | ||
client.dispatch( | ||
{ | ||
path: "/", | ||
method: "GET", | ||
dumpMaxSize: 1024, | ||
}, | ||
handler | ||
); | ||
``` | ||
## Instance Events | ||
@@ -957,0 +989,0 @@ |
@@ -10,3 +10,3 @@ 'use strict' | ||
if (err && typeof err === 'object') { | ||
Error.captureStackTrace(err, this) | ||
Error.captureStackTrace(err) | ||
} | ||
@@ -13,0 +13,0 @@ throw err |
@@ -43,3 +43,4 @@ 'use strict' | ||
redirect: require('./lib/interceptor/redirect'), | ||
retry: require('./lib/interceptor/retry') | ||
retry: require('./lib/interceptor/retry'), | ||
dump: require('./lib/interceptor/dump') | ||
} | ||
@@ -112,3 +113,3 @@ | ||
if (err && typeof err === 'object') { | ||
Error.captureStackTrace(err, this) | ||
Error.captureStackTrace(err) | ||
} | ||
@@ -115,0 +116,0 @@ |
@@ -11,3 +11,2 @@ module.exports = { | ||
kConnecting: Symbol('connecting'), | ||
kHeadersList: Symbol('headers list'), | ||
kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'), | ||
@@ -14,0 +13,0 @@ kKeepAliveMaxTimeout: Symbol('max keep alive timeout'), |
@@ -1104,3 +1104,3 @@ 'use strict' | ||
async function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
try { | ||
@@ -1107,0 +1107,0 @@ if (!body) { |
'use strict' | ||
const assert = require('node:assert') | ||
const { kHeadersList } = require('../../core/symbols') | ||
const { getHeadersList: internalGetHeadersList } = require('../fetch/headers') | ||
@@ -281,4 +281,6 @@ /** | ||
function getHeadersList (headers) { | ||
if (headers[kHeadersList]) { | ||
return headers[kHeadersList] | ||
try { | ||
return internalGetHeadersList(headers) | ||
} catch { | ||
// fall-through | ||
} | ||
@@ -285,0 +287,0 @@ |
@@ -388,2 +388,11 @@ 'use strict' | ||
}, instance, false) | ||
}, | ||
bytes () { | ||
// The bytes() method steps are to return the result of running consume body | ||
// with this and the following step given a byte sequence bytes: return the | ||
// result of creating a Uint8Array from bytes in this’s relevant realm. | ||
return consumeBody(this, (bytes) => { | ||
return new Uint8Array(bytes.buffer, 0, bytes.byteLength) | ||
}, instance, true) | ||
} | ||
@@ -390,0 +399,0 @@ } |
@@ -5,4 +5,3 @@ // https://github.com/Ethan-Arrowood/undici-fetch | ||
const { kHeadersList, kConstruct } = require('../../core/symbols') | ||
const { kGuard } = require('./symbols') | ||
const { kConstruct } = require('../../core/symbols') | ||
const { kEnumerableProperty } = require('../../core/util') | ||
@@ -107,8 +106,7 @@ const { | ||
// forbidden header name, return. | ||
// 5. Otherwise, if headers’s guard is "request-no-cors": | ||
// TODO | ||
// Note: undici does not implement forbidden header names | ||
if (headers[kGuard] === 'immutable') { | ||
if (getHeadersGuard(headers) === 'immutable') { | ||
throw new TypeError('immutable') | ||
} else if (headers[kGuard] === 'request-no-cors') { | ||
// 5. Otherwise, if headers’s guard is "request-no-cors": | ||
// TODO | ||
} | ||
@@ -120,3 +118,3 @@ | ||
// 7. Append (name, value) to headers’s header list. | ||
return headers[kHeadersList].append(name, value, false) | ||
return getHeadersList(headers).append(name, value, false) | ||
@@ -363,2 +361,5 @@ // 8. If headers’s guard is "request-no-cors", then remove | ||
class Headers { | ||
#guard | ||
#headersList | ||
constructor (init = undefined) { | ||
@@ -368,8 +369,9 @@ if (init === kConstruct) { | ||
} | ||
this[kHeadersList] = new HeadersList() | ||
this.#headersList = new HeadersList() | ||
// The new Headers(init) constructor steps are: | ||
// 1. Set this’s guard to "none". | ||
this[kGuard] = 'none' | ||
this.#guard = 'none' | ||
@@ -424,6 +426,4 @@ // 2. If init is given, then fill this with init. | ||
// Note: undici does not implement forbidden header names | ||
if (this[kGuard] === 'immutable') { | ||
if (this.#guard === 'immutable') { | ||
throw new TypeError('immutable') | ||
} else if (this[kGuard] === 'request-no-cors') { | ||
// TODO | ||
} | ||
@@ -433,3 +433,3 @@ | ||
// return. | ||
if (!this[kHeadersList].contains(name, false)) { | ||
if (!this.#headersList.contains(name, false)) { | ||
return | ||
@@ -441,3 +441,3 @@ } | ||
// privileged no-CORS request headers from this. | ||
this[kHeadersList].delete(name, false) | ||
this.#headersList.delete(name, false) | ||
} | ||
@@ -465,3 +465,3 @@ | ||
// list. | ||
return this[kHeadersList].get(name, false) | ||
return this.#headersList.get(name, false) | ||
} | ||
@@ -489,3 +489,3 @@ | ||
// otherwise false. | ||
return this[kHeadersList].contains(name, false) | ||
return this.#headersList.contains(name, false) | ||
} | ||
@@ -531,6 +531,4 @@ | ||
// Note: undici does not implement forbidden header names | ||
if (this[kGuard] === 'immutable') { | ||
if (this.#guard === 'immutable') { | ||
throw new TypeError('immutable') | ||
} else if (this[kGuard] === 'request-no-cors') { | ||
// TODO | ||
} | ||
@@ -541,3 +539,3 @@ | ||
// privileged no-CORS request headers from this | ||
this[kHeadersList].set(name, value, false) | ||
this.#headersList.set(name, value, false) | ||
} | ||
@@ -553,3 +551,3 @@ | ||
const list = this[kHeadersList].cookies | ||
const list = this.#headersList.cookies | ||
@@ -565,4 +563,4 @@ if (list) { | ||
get [kHeadersSortedMap] () { | ||
if (this[kHeadersList][kHeadersSortedMap]) { | ||
return this[kHeadersList][kHeadersSortedMap] | ||
if (this.#headersList[kHeadersSortedMap]) { | ||
return this.#headersList[kHeadersSortedMap] | ||
} | ||
@@ -576,5 +574,5 @@ | ||
// set with all the names of the headers in list. | ||
const names = this[kHeadersList].toSortedArray() | ||
const names = this.#headersList.toSortedArray() | ||
const cookies = this[kHeadersList].cookies | ||
const cookies = this.#headersList.cookies | ||
@@ -584,3 +582,3 @@ // fast-path | ||
// Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray` | ||
return (this[kHeadersList][kHeadersSortedMap] = names) | ||
return (this.#headersList[kHeadersSortedMap] = names) | ||
} | ||
@@ -615,3 +613,3 @@ | ||
// 4. Return headers. | ||
return (this[kHeadersList][kHeadersSortedMap] = headers) | ||
return (this.#headersList[kHeadersSortedMap] = headers) | ||
} | ||
@@ -622,6 +620,28 @@ | ||
return `Headers ${util.formatWithOptions(options, this[kHeadersList].entries)}` | ||
return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}` | ||
} | ||
static getHeadersGuard (o) { | ||
return o.#guard | ||
} | ||
static setHeadersGuard (o, guard) { | ||
o.#guard = guard | ||
} | ||
static getHeadersList (o) { | ||
return o.#headersList | ||
} | ||
static setHeadersList (o, list) { | ||
o.#headersList = list | ||
} | ||
} | ||
const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers | ||
Reflect.deleteProperty(Headers, 'getHeadersGuard') | ||
Reflect.deleteProperty(Headers, 'setHeadersGuard') | ||
Reflect.deleteProperty(Headers, 'getHeadersList') | ||
Reflect.deleteProperty(Headers, 'setHeadersList') | ||
Object.defineProperty(Headers.prototype, util.inspect.custom, { | ||
@@ -652,4 +672,8 @@ enumerable: false | ||
// Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please. | ||
if (!util.types.isProxy(V) && kHeadersList in V && iterator === Headers.prototype.entries) { // Headers object | ||
return V[kHeadersList].entriesList | ||
if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object | ||
try { | ||
return getHeadersList(V).entriesList | ||
} catch { | ||
// fall-through | ||
} | ||
} | ||
@@ -676,3 +700,7 @@ | ||
Headers, | ||
HeadersList | ||
HeadersList, | ||
getHeadersGuard, | ||
setHeadersGuard, | ||
setHeadersList, | ||
getHeadersList | ||
} |
@@ -6,3 +6,3 @@ /* globals AbortController */ | ||
const { extractBody, mixinBody, cloneBody } = require('./body') | ||
const { Headers, fill: fillHeaders, HeadersList } = require('./headers') | ||
const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers') | ||
const { FinalizationRegistry } = require('./dispatcher-weakref')() | ||
@@ -29,6 +29,6 @@ const util = require('../../core/util') | ||
const { kEnumerableProperty } = util | ||
const { kHeaders, kSignal, kState, kGuard, kDispatcher } = require('./symbols') | ||
const { kHeaders, kSignal, kState, kDispatcher } = require('./symbols') | ||
const { webidl } = require('./webidl') | ||
const { URLSerializer } = require('./data-url') | ||
const { kHeadersList, kConstruct } = require('../../core/symbols') | ||
const { kConstruct } = require('../../core/symbols') | ||
const assert = require('node:assert') | ||
@@ -450,4 +450,4 @@ const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('node:events') | ||
this[kHeaders] = new Headers(kConstruct) | ||
this[kHeaders][kHeadersList] = request.headersList | ||
this[kHeaders][kGuard] = 'request' | ||
setHeadersList(this[kHeaders], request.headersList) | ||
setHeadersGuard(this[kHeaders], 'request') | ||
@@ -465,3 +465,3 @@ // 31. If this’s request’s mode is "no-cors", then: | ||
// 2. Set this’s headers’s guard to "request-no-cors". | ||
this[kHeaders][kGuard] = 'request-no-cors' | ||
setHeadersGuard(this[kHeaders], 'request-no-cors') | ||
} | ||
@@ -472,3 +472,3 @@ | ||
/** @type {HeadersList} */ | ||
const headersList = this[kHeaders][kHeadersList] | ||
const headersList = getHeadersList(this[kHeaders]) | ||
// 1. Let headers be a copy of this’s headers and its associated header | ||
@@ -527,3 +527,3 @@ // list. | ||
// this’s headers. | ||
if (contentType && !this[kHeaders][kHeadersList].contains('content-type', true)) { | ||
if (contentType && !getHeadersList(this[kHeaders]).contains('content-type', true)) { | ||
this[kHeaders].append('content-type', contentType) | ||
@@ -794,3 +794,3 @@ } | ||
// 4. Return clonedRequestObject. | ||
return fromInnerRequest(clonedRequest, ac.signal, this[kHeaders][kGuard]) | ||
return fromInnerRequest(clonedRequest, ac.signal, getHeadersGuard(this[kHeaders])) | ||
} | ||
@@ -904,4 +904,4 @@ | ||
request[kHeaders] = new Headers(kConstruct) | ||
request[kHeaders][kHeadersList] = innerRequest.headersList | ||
request[kHeaders][kGuard] = guard | ||
setHeadersList(request[kHeaders], innerRequest.headersList) | ||
setHeadersGuard(request[kHeaders], guard) | ||
return request | ||
@@ -908,0 +908,0 @@ } |
'use strict' | ||
const { Headers, HeadersList, fill } = require('./headers') | ||
const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers') | ||
const { extractBody, cloneBody, mixinBody } = require('./body') | ||
@@ -22,7 +22,7 @@ const util = require('../../core/util') | ||
} = require('./constants') | ||
const { kState, kHeaders, kGuard } = require('./symbols') | ||
const { kState, kHeaders } = require('./symbols') | ||
const { webidl } = require('./webidl') | ||
const { FormData } = require('./formdata') | ||
const { URLSerializer } = require('./data-url') | ||
const { kHeadersList, kConstruct } = require('../../core/symbols') | ||
const { kConstruct } = require('../../core/symbols') | ||
const assert = require('node:assert') | ||
@@ -145,4 +145,4 @@ const { types } = require('node:util') | ||
this[kHeaders] = new Headers(kConstruct) | ||
this[kHeaders][kGuard] = 'response' | ||
this[kHeaders][kHeadersList] = this[kState].headersList | ||
setHeadersGuard(this[kHeaders], 'response') | ||
setHeadersList(this[kHeaders], this[kState].headersList) | ||
@@ -260,3 +260,3 @@ // 3. Let bodyWithType be null. | ||
// clonedResponse, this’s headers’s guard, and this’s relevant Realm. | ||
return fromInnerResponse(clonedResponse, this[kHeaders][kGuard]) | ||
return fromInnerResponse(clonedResponse, getHeadersGuard(this[kHeaders])) | ||
} | ||
@@ -528,4 +528,4 @@ | ||
response[kHeaders] = new Headers(kConstruct) | ||
response[kHeaders][kHeadersList] = innerResponse.headersList | ||
response[kHeaders][kGuard] = guard | ||
setHeadersList(response[kHeaders], innerResponse.headersList) | ||
setHeadersGuard(response[kHeaders], guard) | ||
@@ -532,0 +532,0 @@ if (hasFinalizationRegistry && innerResponse.body?.stream) { |
@@ -8,4 +8,3 @@ 'use strict' | ||
kState: Symbol('state'), | ||
kGuard: Symbol('guard'), | ||
kDispatcher: Symbol('dispatcher') | ||
} |
@@ -16,5 +16,4 @@ 'use strict' | ||
const { fetching } = require('../fetch/index') | ||
const { Headers } = require('../fetch/headers') | ||
const { Headers, getHeadersList } = require('../fetch/headers') | ||
const { getDecodeSplit } = require('../fetch/util') | ||
const { kHeadersList } = require('../../core/symbols') | ||
const { WebsocketFrameSend } = require('./frame') | ||
@@ -63,3 +62,3 @@ | ||
if (options.headers) { | ||
const headersList = new Headers(options.headers)[kHeadersList] | ||
const headersList = getHeadersList(new Headers(options.headers)) | ||
@@ -266,9 +265,5 @@ request.headersList = headersList | ||
socket.write(frame.createFrame(opcodes.CLOSE), (err) => { | ||
if (!err) { | ||
ws[kSentClose] = sentCloseFrameState.SENT | ||
} | ||
}) | ||
socket.write(frame.createFrame(opcodes.CLOSE)) | ||
ws[kSentClose] = sentCloseFrameState.PROCESSING | ||
ws[kSentClose] = sentCloseFrameState.SENT | ||
@@ -275,0 +270,0 @@ // Upon either sending or receiving a Close control frame, it is said |
'use strict' | ||
const { Writable } = require('node:stream') | ||
const assert = require('node:assert') | ||
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants') | ||
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols') | ||
const { channels } = require('../../core/diagnostics') | ||
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require('./util') | ||
const { | ||
isValidStatusCode, | ||
isValidOpcode, | ||
failWebsocketConnection, | ||
websocketMessageReceived, | ||
utf8Decode, | ||
isControlFrame, | ||
isTextBinaryFrame, | ||
isContinuationFrame | ||
} = require('./util') | ||
const { WebsocketFrameSend } = require('./frame') | ||
const { CloseEvent } = require('./events') | ||
const { closeWebSocketConnection } = require('./connection') | ||
@@ -19,2 +29,3 @@ // This code was influenced by ws released under the MIT license. | ||
#byteOffset = 0 | ||
#loop = false | ||
@@ -39,2 +50,3 @@ #state = parserStates.INFO | ||
this.#byteOffset += chunk.length | ||
this.#loop = true | ||
@@ -50,3 +62,3 @@ this.run(callback) | ||
run (callback) { | ||
while (true) { | ||
while (this.#loop) { | ||
if (this.#state === parserStates.INFO) { | ||
@@ -59,8 +71,19 @@ // If there aren't enough bytes to parse the payload length, etc. | ||
const buffer = this.consume(2) | ||
const fin = (buffer[0] & 0x80) !== 0 | ||
const opcode = buffer[0] & 0x0F | ||
const masked = (buffer[1] & 0x80) === 0x80 | ||
this.#info.fin = (buffer[0] & 0x80) !== 0 | ||
this.#info.opcode = buffer[0] & 0x0F | ||
this.#info.masked = (buffer[1] & 0x80) === 0x80 | ||
const fragmented = !fin && opcode !== opcodes.CONTINUATION | ||
const payloadLength = buffer[1] & 0x7F | ||
if (this.#info.masked) { | ||
const rsv1 = buffer[0] & 0x40 | ||
const rsv2 = buffer[0] & 0x20 | ||
const rsv3 = buffer[0] & 0x10 | ||
if (!isValidOpcode(opcode)) { | ||
failWebsocketConnection(this.ws, 'Invalid opcode received') | ||
return callback() | ||
} | ||
if (masked) { | ||
failWebsocketConnection(this.ws, 'Frame cannot be masked') | ||
@@ -70,9 +93,13 @@ return callback() | ||
// If we receive a fragmented message, we use the type of the first | ||
// frame to parse the full message as binary/text, when it's terminated | ||
this.#info.originalOpcode ??= this.#info.opcode | ||
// MUST be 0 unless an extension is negotiated that defines meanings | ||
// for non-zero values. If a nonzero value is received and none of | ||
// the negotiated extensions defines the meaning of such a nonzero | ||
// value, the receiving endpoint MUST _Fail the WebSocket | ||
// Connection_. | ||
if (rsv1 !== 0 || rsv2 !== 0 || rsv3 !== 0) { | ||
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear') | ||
return | ||
} | ||
this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION | ||
if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) { | ||
if (fragmented && !isTextBinaryFrame(opcode)) { | ||
// Only text and binary frames can be fragmented | ||
@@ -83,4 +110,27 @@ failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.') | ||
const payloadLength = buffer[1] & 0x7F | ||
// If we are already parsing a text/binary frame and do not receive either | ||
// a continuation frame or close frame, fail the connection. | ||
if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) { | ||
failWebsocketConnection(this.ws, 'Expected continuation frame') | ||
return | ||
} | ||
if (this.#info.fragmented && fragmented) { | ||
// A fragmented frame can't be fragmented itself | ||
failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.') | ||
return | ||
} | ||
// "All control frames MUST have a payload length of 125 bytes or less | ||
// and MUST NOT be fragmented." | ||
if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) { | ||
failWebsocketConnection(this.ws, 'Control frame either too large or fragmented') | ||
return | ||
} | ||
if (isContinuationFrame(opcode) && this.#fragments.length === 0) { | ||
failWebsocketConnection(this.ws, 'Unexpected continuation frame') | ||
return | ||
} | ||
if (payloadLength <= 125) { | ||
@@ -95,111 +145,10 @@ this.#info.payloadLength = payloadLength | ||
if (this.#info.fragmented && payloadLength > 125) { | ||
// A fragmented frame can't be fragmented itself | ||
failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.') | ||
return | ||
} else if ( | ||
(this.#info.opcode === opcodes.PING || | ||
this.#info.opcode === opcodes.PONG || | ||
this.#info.opcode === opcodes.CLOSE) && | ||
payloadLength > 125 | ||
) { | ||
// Control frames can have a payload length of 125 bytes MAX | ||
failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.') | ||
return | ||
} else if (this.#info.opcode === opcodes.CLOSE) { | ||
if (payloadLength === 1) { | ||
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.') | ||
return | ||
} | ||
if (isTextBinaryFrame(opcode)) { | ||
this.#info.binaryType = opcode | ||
} | ||
const body = this.consume(payloadLength) | ||
this.#info.closeInfo = this.parseCloseBody(body) | ||
if (this.#info.closeInfo.error) { | ||
const { code, reason } = this.#info.closeInfo | ||
callback(new CloseEvent('close', { wasClean: false, reason, code })) | ||
return | ||
} | ||
if (this.ws[kSentClose] !== sentCloseFrameState.SENT) { | ||
// If an endpoint receives a Close frame and did not previously send a | ||
// Close frame, the endpoint MUST send a Close frame in response. (When | ||
// sending a Close frame in response, the endpoint typically echos the | ||
// status code it received.) | ||
let body = emptyBuffer | ||
if (this.#info.closeInfo.code) { | ||
body = Buffer.allocUnsafe(2) | ||
body.writeUInt16BE(this.#info.closeInfo.code, 0) | ||
} | ||
const closeFrame = new WebsocketFrameSend(body) | ||
this.ws[kResponse].socket.write( | ||
closeFrame.createFrame(opcodes.CLOSE), | ||
(err) => { | ||
if (!err) { | ||
this.ws[kSentClose] = sentCloseFrameState.SENT | ||
} | ||
} | ||
) | ||
} | ||
// Upon either sending or receiving a Close control frame, it is said | ||
// that _The WebSocket Closing Handshake is Started_ and that the | ||
// WebSocket connection is in the CLOSING state. | ||
this.ws[kReadyState] = states.CLOSING | ||
this.ws[kReceivedClose] = true | ||
this.end() | ||
return | ||
} else if (this.#info.opcode === opcodes.PING) { | ||
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in | ||
// response, unless it already received a Close frame. | ||
// A Pong frame sent in response to a Ping frame must have identical | ||
// "Application data" | ||
const body = this.consume(payloadLength) | ||
if (!this.ws[kReceivedClose]) { | ||
const frame = new WebsocketFrameSend(body) | ||
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG)) | ||
if (channels.ping.hasSubscribers) { | ||
channels.ping.publish({ | ||
payload: body | ||
}) | ||
} | ||
} | ||
this.#state = parserStates.INFO | ||
if (this.#byteOffset > 0) { | ||
continue | ||
} else { | ||
callback() | ||
return | ||
} | ||
} else if (this.#info.opcode === opcodes.PONG) { | ||
// A Pong frame MAY be sent unsolicited. This serves as a | ||
// unidirectional heartbeat. A response to an unsolicited Pong frame is | ||
// not expected. | ||
const body = this.consume(payloadLength) | ||
if (channels.pong.hasSubscribers) { | ||
channels.pong.publish({ | ||
payload: body | ||
}) | ||
} | ||
if (this.#byteOffset > 0) { | ||
continue | ||
} else { | ||
callback() | ||
return | ||
} | ||
} | ||
this.#info.opcode = opcode | ||
this.#info.masked = masked | ||
this.#info.fin = fin | ||
this.#info.fragmented = fragmented | ||
} else if (this.#state === parserStates.PAYLOADLENGTH_16) { | ||
@@ -239,29 +188,24 @@ if (this.#byteOffset < 2) { | ||
if (this.#byteOffset < this.#info.payloadLength) { | ||
// If there is still more data in this chunk that needs to be read | ||
return callback() | ||
} else if (this.#byteOffset >= this.#info.payloadLength) { | ||
// If the server sent multiple frames in a single chunk | ||
} | ||
const body = this.consume(this.#info.payloadLength) | ||
const body = this.consume(this.#info.payloadLength) | ||
if (isControlFrame(this.#info.opcode)) { | ||
this.#loop = this.parseControlFrame(body) | ||
} else { | ||
this.#fragments.push(body) | ||
// If the frame is unfragmented, or a fragmented frame was terminated, | ||
// a message was received | ||
if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) { | ||
// If the frame is not fragmented, a message has been received. | ||
// If the frame is fragmented, it will terminate with a fin bit set | ||
// and an opcode of 0 (continuation), therefore we handle that when | ||
// parsing continuation frames, not here. | ||
if (!this.#info.fragmented && this.#info.fin) { | ||
const fullMessage = Buffer.concat(this.#fragments) | ||
websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage) | ||
this.#info = {} | ||
websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage) | ||
this.#fragments.length = 0 | ||
} | ||
this.#state = parserStates.INFO | ||
} | ||
} | ||
if (this.#byteOffset === 0 && this.#info.payloadLength !== 0) { | ||
callback() | ||
break | ||
this.#state = parserStates.INFO | ||
} | ||
@@ -274,7 +218,7 @@ } | ||
* @param {number} n | ||
* @returns {Buffer|null} | ||
* @returns {Buffer} | ||
*/ | ||
consume (n) { | ||
if (n > this.#byteOffset) { | ||
return null | ||
throw new Error('Called consume() before buffers satiated.') | ||
} else if (n === 0) { | ||
@@ -315,2 +259,4 @@ return emptyBuffer | ||
parseCloseBody (data) { | ||
assert(data.length !== 1) | ||
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 | ||
@@ -349,2 +295,87 @@ /** @type {number|undefined} */ | ||
/** | ||
* Parses control frames. | ||
* @param {Buffer} body | ||
*/ | ||
parseControlFrame (body) { | ||
const { opcode, payloadLength } = this.#info | ||
if (opcode === opcodes.CLOSE) { | ||
if (payloadLength === 1) { | ||
failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.') | ||
return false | ||
} | ||
this.#info.closeInfo = this.parseCloseBody(body) | ||
if (this.#info.closeInfo.error) { | ||
const { code, reason } = this.#info.closeInfo | ||
closeWebSocketConnection(this.ws, code, reason, reason.length) | ||
failWebsocketConnection(this.ws, reason) | ||
return false | ||
} | ||
if (this.ws[kSentClose] !== sentCloseFrameState.SENT) { | ||
// If an endpoint receives a Close frame and did not previously send a | ||
// Close frame, the endpoint MUST send a Close frame in response. (When | ||
// sending a Close frame in response, the endpoint typically echos the | ||
// status code it received.) | ||
let body = emptyBuffer | ||
if (this.#info.closeInfo.code) { | ||
body = Buffer.allocUnsafe(2) | ||
body.writeUInt16BE(this.#info.closeInfo.code, 0) | ||
} | ||
const closeFrame = new WebsocketFrameSend(body) | ||
this.ws[kResponse].socket.write( | ||
closeFrame.createFrame(opcodes.CLOSE), | ||
(err) => { | ||
if (!err) { | ||
this.ws[kSentClose] = sentCloseFrameState.SENT | ||
} | ||
} | ||
) | ||
} | ||
// Upon either sending or receiving a Close control frame, it is said | ||
// that _The WebSocket Closing Handshake is Started_ and that the | ||
// WebSocket connection is in the CLOSING state. | ||
this.ws[kReadyState] = states.CLOSING | ||
this.ws[kReceivedClose] = true | ||
this.end() | ||
return false | ||
} else if (opcode === opcodes.PING) { | ||
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in | ||
// response, unless it already received a Close frame. | ||
// A Pong frame sent in response to a Ping frame must have identical | ||
// "Application data" | ||
if (!this.ws[kReceivedClose]) { | ||
const frame = new WebsocketFrameSend(body) | ||
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG)) | ||
if (channels.ping.hasSubscribers) { | ||
channels.ping.publish({ | ||
payload: body | ||
}) | ||
} | ||
} | ||
} else if (opcode === opcodes.PONG) { | ||
// A Pong frame MAY be sent unsolicited. This serves as a | ||
// unidirectional heartbeat. A response to an unsolicited Pong frame is | ||
// not expected. | ||
if (channels.pong.hasSubscribers) { | ||
channels.pong.publish({ | ||
payload: body | ||
}) | ||
} | ||
} | ||
return true | ||
} | ||
get closingInfo () { | ||
@@ -351,0 +382,0 @@ return this.#info.closeInfo |
@@ -213,2 +213,26 @@ 'use strict' | ||
/** | ||
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5 | ||
* @param {number} opcode | ||
*/ | ||
function isControlFrame (opcode) { | ||
return ( | ||
opcode === opcodes.CLOSE || | ||
opcode === opcodes.PING || | ||
opcode === opcodes.PONG | ||
) | ||
} | ||
function isContinuationFrame (opcode) { | ||
return opcode === opcodes.CONTINUATION | ||
} | ||
function isTextBinaryFrame (opcode) { | ||
return opcode === opcodes.TEXT || opcode === opcodes.BINARY | ||
} | ||
function isValidOpcode (opcode) { | ||
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode) | ||
} | ||
// https://nodejs.org/api/intl.html#detecting-internationalization-support | ||
@@ -241,3 +265,7 @@ const hasIntl = typeof process.versions.icu === 'string' | ||
websocketMessageReceived, | ||
utf8Decode | ||
utf8Decode, | ||
isControlFrame, | ||
isContinuationFrame, | ||
isTextBinaryFrame, | ||
isValidOpcode | ||
} |
@@ -29,3 +29,3 @@ 'use strict' | ||
const { types } = require('node:util') | ||
const { ErrorEvent } = require('./events') | ||
const { ErrorEvent, CloseEvent } = require('./events') | ||
@@ -291,3 +291,3 @@ let experimentalWarned = false | ||
const ab = new FastBuffer(data, data.byteOffset, data.byteLength) | ||
const ab = new FastBuffer(data.buffer, data.byteOffset, data.byteLength) | ||
@@ -599,5 +599,15 @@ const frame = new WebsocketFrameSend(ab) | ||
function onParserError (err) { | ||
fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason })) | ||
let message | ||
let code | ||
closeWebSocketConnection(this, err.code) | ||
if (err instanceof CloseEvent) { | ||
message = err.reason | ||
code = err.code | ||
} else { | ||
message = err.message | ||
} | ||
fireEvent('error', this, () => new ErrorEvent('error', { error: err, message })) | ||
closeWebSocketConnection(this, code) | ||
} | ||
@@ -604,0 +614,0 @@ |
{ | ||
"name": "undici", | ||
"version": "6.16.1", | ||
"version": "6.17.0", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -91,2 +91,4 @@ "homepage": "https://undici.nodejs.org", | ||
"test:websocket": "borp -p \"test/websocket/*.js\"", | ||
"test:websocket:autobahn": "node test/autobahn/client.js", | ||
"test:websocket:autobahn:report": "node test/autobahn/report.js", | ||
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", | ||
@@ -139,3 +141,3 @@ "test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", | ||
"lib/llhttp/utils.js", | ||
"test/wpt/tests" | ||
"test/fixtures/wpt" | ||
] | ||
@@ -142,0 +144,0 @@ }, |
@@ -22,4 +22,4 @@ import { URL } from 'url' | ||
/** Compose a chain of dispatchers */ | ||
compose(dispatchers: Dispatcher.DispatcherInterceptor[]): Dispatcher.ComposedDispatcher; | ||
compose(...dispatchers: Dispatcher.DispatcherInterceptor[]): Dispatcher.ComposedDispatcher; | ||
compose(dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher; | ||
compose(...dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher; | ||
/** Performs an HTTP request. */ | ||
@@ -101,3 +101,3 @@ request(options: Dispatcher.RequestOptions): Promise<Dispatcher.ResponseData>; | ||
export interface ComposedDispatcher extends Dispatcher {} | ||
export type DispatcherInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch']; | ||
export type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch']; | ||
export interface DispatchOptions { | ||
@@ -104,0 +104,0 @@ origin?: string | URL; |
@@ -69,2 +69,7 @@ import Dispatcher from'./dispatcher' | ||
var caches: typeof import('./cache').caches; | ||
var interceptors: { | ||
dump: typeof import('./interceptors').dump; | ||
retry: typeof import('./interceptors').retry; | ||
redirect: typeof import('./interceptors').redirect; | ||
} | ||
} |
import Dispatcher from "./dispatcher"; | ||
import RetryHandler from "./retry-handler"; | ||
type RedirectInterceptorOpts = { maxRedirections?: number } | ||
export type DumpInterceptorOpts = { maxSize?: number } | ||
export type RetryInterceptorOpts = RetryHandler.RetryOptions | ||
export type RedirectInterceptorOpts = { maxRedirections?: number } | ||
export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.DispatchInterceptor | ||
export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor | ||
export declare function dump(opts?: DumpInterceptorOpts): Dispatcher.DispatcherComposeInterceptor | ||
export declare function retry(opts?: RetryInterceptorOpts): Dispatcher.DispatcherComposeInterceptor | ||
export declare function redirect(opts?: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor |
1123875
172
23700