Comparing version 4.0.0 to 4.1.0
@@ -197,3 +197,3 @@ # Dispatcher | ||
* **method** `string` | ||
* **body** `string | Buffer | Uint8Array | stream.Readable | null` (optional) - Default: `null` | ||
* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` | ||
* **headers** `UndiciHeaders` (optional) - Default: `null` | ||
@@ -200,0 +200,0 @@ * **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. |
@@ -36,3 +36,3 @@ # Class: MockAgent | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { Agent, MockAgent } = require('undici') | ||
@@ -73,3 +73,3 @@ const agent = new Agent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -102,3 +102,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, request } = require('undici') | ||
@@ -129,3 +129,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, request } = require('undici') | ||
@@ -156,3 +156,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, request } = require('undici') | ||
@@ -183,3 +183,3 @@ const mockAgent = new MockAgent({ connections: 1 }) | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -211,3 +211,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -230,3 +230,3 @@ const mockAgent = new MockAgent() | ||
headers, | ||
tailers, | ||
trailers, | ||
body | ||
@@ -252,3 +252,3 @@ } = await request('http://localhost:3000/foo?hello=there&see=ya', { | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -280,3 +280,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -286,6 +286,6 @@ const mockAgent = new MockAgent() | ||
const mockPool = mockAgent.get((origin) => 'http://localhost:3000' === origin)) | ||
const mockPool = mockAgent.get((origin) => origin === 'http://localhost:3000') | ||
mockPool.intercept({ | ||
path: '/foo', | ||
method: 'GET', | ||
method: 'GET' | ||
}).reply(200, 'foo') | ||
@@ -315,3 +315,3 @@ | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher } = require('undici') | ||
@@ -343,3 +343,3 @@ const mockAgent = new MockAgent() | ||
path: '/foo', | ||
method: 'GET', | ||
method: 'GET' | ||
}).reply(200, 'foo') | ||
@@ -367,3 +367,3 @@ | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher } = require('undici') | ||
@@ -386,3 +386,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher } = require('undici') | ||
@@ -415,3 +415,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -431,3 +431,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -454,3 +454,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -470,3 +470,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
@@ -492,3 +492,3 @@ const mockAgent = new MockAgent() | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, request } = require('undici') | ||
@@ -495,0 +495,0 @@ const mockAgent = new MockAgent() |
@@ -91,5 +91,5 @@ # Class: MockPool | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -121,5 +121,5 @@ | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -131,3 +131,3 @@ | ||
path: '/foo', | ||
method: 'GET', | ||
method: 'GET' | ||
}).reply(200, 'foo') | ||
@@ -137,6 +137,5 @@ | ||
path: '/hello', | ||
method: 'GET', | ||
method: 'GET' | ||
}).reply(200, 'hello') | ||
const result1 = await request('http://localhost:3000/foo') | ||
@@ -151,5 +150,5 @@ | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -174,3 +173,3 @@ | ||
headers, | ||
tailers, | ||
trailers, | ||
body | ||
@@ -201,5 +200,5 @@ } = await request('http://localhost:3000/foo?hello=there&see=ya', { | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -235,5 +234,5 @@ | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -244,8 +243,8 @@ | ||
mockPool.intercept({ | ||
path: '/foo', | ||
method: 'GET' | ||
}).replyWithError(new Error('kaboom')) | ||
path: '/foo', | ||
method: 'GET' | ||
}).replyWithError(new Error('kaboom')) | ||
await request('http://localhost:3000/foo', { | ||
method: 'GET', | ||
method: 'GET' | ||
}) | ||
@@ -259,5 +258,5 @@ // Will throw new Error('kaboom') | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -269,3 +268,3 @@ | ||
path: '/foo', | ||
method: 'GET', | ||
method: 'GET' | ||
}).defaultReplyHeaders({ foo: 'bar' }) | ||
@@ -282,5 +281,5 @@ .reply(200, 'foo') | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -292,3 +291,3 @@ | ||
path: '/foo', | ||
method: 'GET', | ||
method: 'GET' | ||
}).defaultReplyTrailers({ foo: 'bar' }) | ||
@@ -305,5 +304,5 @@ .reply(200, 'foo') | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -314,5 +313,5 @@ | ||
mockPool.intercept({ | ||
path: '/foo', | ||
method: 'GET' | ||
}).replyContentLength().reply(200, 'foo') | ||
path: '/foo', | ||
method: 'GET' | ||
}).replyContentLength().reply(200, 'foo') | ||
@@ -327,5 +326,5 @@ const { headers } = await request('http://localhost:3000/foo') | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -336,8 +335,8 @@ | ||
mockPool.intercept({ | ||
path: '/foo', | ||
method: 'GET' | ||
}).replyContentLength().reply(200, 'foo') | ||
path: '/foo', | ||
method: 'GET' | ||
}).replyContentLength().reply(200, { foo: 'bar' }) | ||
const { headers } = await request('http://localhost:3000/foo') | ||
// headers: {"content-length":"3"} | ||
// headers: {"content-length":"13"} | ||
``` | ||
@@ -349,5 +348,5 @@ | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -358,5 +357,5 @@ | ||
mockPool.intercept({ | ||
path: '/foo', | ||
method: 'GET' | ||
}).reply(200, 'foo').persist() | ||
path: '/foo', | ||
method: 'GET' | ||
}).reply(200, 'foo').persist() | ||
@@ -376,5 +375,5 @@ const result1 = await request('http://localhost:3000/foo') | ||
'use strict' | ||
const { MockAgent } = require('undici') | ||
const { MockAgent, setGlobalDispatcher, request } = require('undici') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
setGlobalDispatcher(mockAgent) | ||
@@ -385,5 +384,5 @@ | ||
mockPool.intercept({ | ||
path: '/foo', | ||
method: 'GET' | ||
}).reply(200, 'foo').times(2) | ||
path: '/foo', | ||
method: 'GET' | ||
}).reply(200, 'foo').times(2) | ||
@@ -412,3 +411,3 @@ const result1 = await request('http://localhost:3000/foo') | ||
const mockAgent = new MockAgent({ connections: 1 }) | ||
const mockAgent = new MockAgent() | ||
const mockPool = mockAgent.get('http://localhost:3000') | ||
@@ -435,4 +434,4 @@ | ||
const mockClient = mockAgent.get('http://localhost:3000') | ||
mockClient.intercept({ | ||
const mockPool = mockAgent.get('http://localhost:3000') | ||
mockPool.intercept({ | ||
path: '/foo', | ||
@@ -445,3 +444,3 @@ method: 'GET', | ||
body | ||
} = await mockClient.request({ | ||
} = await mockPool.request({ | ||
origin: 'http://localhost:3000', | ||
@@ -448,0 +447,0 @@ path: '/foo', |
@@ -77,2 +77,3 @@ 'use strict' | ||
const client = ref.deref() | ||
/* istanbul ignore next: gc is undeterministic */ | ||
if (client) { | ||
@@ -120,3 +121,3 @@ ret += client[kRunning] | ||
const { maxRedirections = this[kMaxRedirections] } = opts | ||
let { maxRedirections = this[kMaxRedirections] } = opts | ||
@@ -127,22 +128,24 @@ if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { | ||
if (!maxRedirections) { | ||
return dispatcher.dispatch(opts, handler) | ||
} | ||
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { | ||
if (maxRedirections && util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { | ||
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp | ||
// so that it can be dispatched again? | ||
// TODO (fix): Do we need 100-expect support to provide a way to do this properly? | ||
return dispatcher.dispatch(opts, handler) | ||
maxRedirections = 0 | ||
} | ||
/* istanbul ignore next */ | ||
if (util.isStream(opts.body)) { | ||
opts.body | ||
.on('data', function () { | ||
assert(false) | ||
}) | ||
if (maxRedirections) { | ||
opts = { ...opts, maxRedirections: 0 } | ||
handler = new RedirectHandler(this, maxRedirections, opts, handler) | ||
/* istanbul ignore next */ | ||
// TODO (fix): Write a comment why this is needed? | ||
if (util.isStream(opts.body)) { | ||
opts.body | ||
.on('data', function () { | ||
assert(false) | ||
}) | ||
} | ||
} | ||
return dispatcher.dispatch(opts, new RedirectHandler(this, opts, handler)) | ||
return dispatcher.dispatch(opts, handler) | ||
} catch (err) { | ||
@@ -149,0 +152,0 @@ if (typeof handler.onError !== 'function') { |
@@ -120,5 +120,3 @@ 'use strict' | ||
if (trailers) { | ||
util.parseHeaders(trailers, this.trailers) | ||
} | ||
util.parseHeaders(trailers, this.trailers) | ||
@@ -125,0 +123,0 @@ res.push(null) |
@@ -135,3 +135,3 @@ 'use strict' | ||
this.trailers = trailers ? util.parseHeaders(trailers) : {} | ||
this.trailers = util.parseHeaders(trailers) | ||
@@ -138,0 +138,0 @@ res.end() |
@@ -10,2 +10,3 @@ 'use strict' | ||
const Dispatcher = require('./dispatcher') | ||
const RedirectHandler = require('./handler/redirect') | ||
const { | ||
@@ -65,5 +66,8 @@ RequestContentLengthMismatchError, | ||
kStrictContentLength, | ||
kConnector | ||
kConnector, | ||
kMaxRedirections | ||
} = require('./core/symbols') | ||
function noop () {} | ||
class Client extends Dispatcher { | ||
@@ -88,2 +92,3 @@ constructor (url, { | ||
maxCachedSessions, | ||
maxRedirections, | ||
[kConnect]: connect | ||
@@ -149,2 +154,6 @@ } = {}) { | ||
if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { | ||
throw new InvalidArgumentError('maxRedirections must be a positive number') | ||
} | ||
this[kUrl] = util.parseOrigin(url) | ||
@@ -170,2 +179,3 @@ this[kConnector] = connect || makeConnect({ tls, socketPath, maxCachedSessions }) | ||
this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength | ||
this[kMaxRedirections] = maxRedirections | ||
@@ -242,2 +252,17 @@ // kQueue is built up of 3 sections separated by | ||
try { | ||
let { maxRedirections = this[kMaxRedirections] } = opts | ||
if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { | ||
throw new InvalidArgumentError('maxRedirections must be a positive number') | ||
} | ||
if (maxRedirections && util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { | ||
maxRedirections = 0 | ||
} | ||
if (maxRedirections) { | ||
opts = { ...opts, maxRedirections: 0 } | ||
handler = new RedirectHandler(this, maxRedirections, opts, handler) | ||
} | ||
const request = new Request(opts, handler) | ||
@@ -256,4 +281,4 @@ | ||
// Do nothing. | ||
} else if (util.isStream(request.body)) { | ||
// Wait a tick in case stream is ended in the same tick. | ||
} else if (util.isStream(request.body) || util.isStrictIterable(request.body)) { | ||
// Wait a tick in case stream/iterator is ended in the same tick. | ||
this[kResuming] = 1 | ||
@@ -365,3 +390,2 @@ process.nextTick(resume, this) | ||
let mod, build | ||
const { resolve } = require('path') | ||
@@ -372,7 +396,8 @@ const { readFileSync } = require('fs') | ||
let mod | ||
try { | ||
build = resolve(__dirname, './llhttp/llhttp_simd.wasm') | ||
const bin = readFileSync(build) | ||
mod = new WebAssembly.Module(bin) | ||
mod = new WebAssembly.Module(readFileSync(resolve(__dirname, './llhttp/llhttp_simd.wasm'))) | ||
} catch (e) { | ||
/* istanbul ignore next */ | ||
// We could check if the error was caused by the simd option not | ||
@@ -382,5 +407,3 @@ // being enabled, but the occurring of this other error | ||
// got me to remove that check to avoid breaking Node 12. | ||
build = resolve(__dirname, './llhttp/llhttp.wasm') | ||
const bin = readFileSync(build) | ||
mod = new WebAssembly.Module(bin) | ||
mod = new WebAssembly.Module(readFileSync(resolve(__dirname, './llhttp/llhttp.wasm'))) | ||
} | ||
@@ -392,2 +415,9 @@ | ||
wasm_on_url: (p, at, len) => { | ||
/* istanbul ignore next */ | ||
return 0 | ||
}, | ||
wasm_on_status: (p, at, len) => { | ||
return 0 | ||
}, | ||
wasm_on_message_begin: (p) => { | ||
@@ -560,2 +590,3 @@ assert.strictEqual(currentParser.ptr, p) | ||
let message = '' | ||
/* istanbul ignore else: difficult to make a test case for */ | ||
if (ptr) { | ||
@@ -751,2 +782,4 @@ const len = new Uint8Array(llhttp.exports.memory.buffer, ptr).indexOf(0) | ||
const request = client[kQueue][client[kRunningIdx]] | ||
/* istanbul ignore next: difficult to make a test case for */ | ||
if (!request) { | ||
@@ -1268,4 +1301,5 @@ return -1 | ||
if (client[kRunning] > 0 && util.isStream(request.body)) { | ||
// Request with stream body can error while other requests | ||
if (client[kRunning] > 0 && | ||
(util.isStream(request.body) || util.isStrictIterable(request.body))) { | ||
// Request with stream or iterator body can error while other requests | ||
// are inflight and indirectly error those as well. | ||
@@ -1275,3 +1309,3 @@ // Ensure this doesn't happen by waiting for inflight | ||
// Request with stream body cannot be retried. | ||
// Request with stream or iterator body cannot be retried. | ||
// Ensure that no other requests are inflight and | ||
@@ -1397,2 +1431,3 @@ // could cause failure. | ||
/* istanbul ignore else: assertion */ | ||
if (!body) { | ||
@@ -1417,146 +1452,283 @@ if (contentLength === 0) { | ||
} | ||
} else if (util.isStream(body)) { | ||
writeStream({ client, request, socket, contentLength, header, expectsPayload }) | ||
} else if (util.isStrictIterable(body)) { | ||
writeIterable({ client, request, socket, contentLength, header, expectsPayload }) | ||
} else { | ||
socket[kWriting] = true | ||
assert(false) | ||
} | ||
assert(util.isStream(body)) | ||
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') | ||
return true | ||
} | ||
let finished = false | ||
let bytesWritten = 0 | ||
function writeStream ({ client, request, socket, contentLength, header, expectsPayload }) { | ||
const { body } = request | ||
const onData = function (chunk) { | ||
try { | ||
assert(!finished) | ||
socket[kWriting] = true | ||
const len = Buffer.byteLength(chunk) | ||
if (!len) { | ||
return | ||
} | ||
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') | ||
// TODO: What if not ended and bytesWritten === contentLength? | ||
// We should defer writing chunks. | ||
if (contentLength !== null && bytesWritten + len > contentLength) { | ||
if (client[kStrictContentLength]) { | ||
util.destroy(socket, new RequestContentLengthMismatchError()) | ||
return | ||
} | ||
let finished = false | ||
let bytesWritten = 0 | ||
process.emitWarning(new RequestContentLengthMismatchError()) | ||
const onData = function (chunk) { | ||
try { | ||
assert(!finished) | ||
const len = Buffer.byteLength(chunk) | ||
if (!len) { | ||
return | ||
} | ||
// TODO: What if not ended and bytesWritten === contentLength? | ||
// We should defer writing chunks. | ||
if (contentLength !== null && bytesWritten + len > contentLength) { | ||
if (client[kStrictContentLength]) { | ||
util.destroy(socket, new RequestContentLengthMismatchError()) | ||
return | ||
} | ||
if (bytesWritten === 0) { | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
process.emitWarning(new RequestContentLengthMismatchError()) | ||
} | ||
if (contentLength === null) { | ||
socket.write(`${header}transfer-encoding: chunked\r\n`, 'ascii') | ||
} else { | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') | ||
} | ||
if (bytesWritten === 0) { | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
if (contentLength === null) { | ||
socket.write(`\r\n${len.toString(16)}\r\n`, 'ascii') | ||
socket.write(`${header}transfer-encoding: chunked\r\n`, 'ascii') | ||
} else { | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') | ||
} | ||
} | ||
bytesWritten += len | ||
if (contentLength === null) { | ||
socket.write(`\r\n${len.toString(16)}\r\n`, 'ascii') | ||
} | ||
if (!socket.write(chunk) && this.pause) { | ||
this.pause() | ||
} | ||
} catch (err) { | ||
util.destroy(this, err) | ||
bytesWritten += len | ||
if (!socket.write(chunk) && this.pause) { | ||
this.pause() | ||
} | ||
} catch (err) { | ||
util.destroy(this, err) | ||
} | ||
const onDrain = function () { | ||
assert(!finished) | ||
} | ||
const onDrain = function () { | ||
assert(!finished) | ||
if (body.resume) { | ||
body.resume() | ||
} | ||
if (body.resume) { | ||
body.resume() | ||
} | ||
const onAbort = function () { | ||
onFinished(new RequestAbortedError()) | ||
} | ||
const onAbort = function () { | ||
onFinished(new RequestAbortedError()) | ||
} | ||
const onFinished = function (err) { | ||
if (finished) { | ||
return | ||
} | ||
const onFinished = function (err) { | ||
if (finished) { | ||
return | ||
} | ||
finished = true | ||
finished = true | ||
assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) | ||
socket[kWriting] = false | ||
assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) | ||
socket[kWriting] = false | ||
if (!err && contentLength !== null && bytesWritten !== contentLength) { | ||
if (client[kStrictContentLength]) { | ||
err = new RequestContentLengthMismatchError() | ||
} else { | ||
process.emitWarning(new RequestContentLengthMismatchError()) | ||
} | ||
if (!err && contentLength !== null && bytesWritten !== contentLength) { | ||
if (client[kStrictContentLength]) { | ||
err = new RequestContentLengthMismatchError() | ||
} else { | ||
process.emitWarning(new RequestContentLengthMismatchError()) | ||
} | ||
} | ||
socket | ||
.removeListener('drain', onDrain) | ||
.removeListener('error', onFinished) | ||
body | ||
.removeListener('data', onData) | ||
.removeListener('end', onFinished) | ||
.removeListener('error', onFinished) | ||
.removeListener('close', onAbort) | ||
socket | ||
.removeListener('drain', onDrain) | ||
.removeListener('error', onFinished) | ||
body | ||
.removeListener('data', onData) | ||
.removeListener('end', onFinished) | ||
.removeListener('error', onFinished) | ||
.removeListener('close', onAbort) | ||
// TODO (fix): Avoid using err.message for logic. | ||
if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { | ||
util.destroy(body, err) | ||
// TODO (fix): Avoid using err.message for logic. | ||
if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { | ||
util.destroy(body, err) | ||
} else { | ||
util.destroy(body) | ||
} | ||
if (err) { | ||
assert(client[kRunning] <= 1, 'pipeline should only contain this request') | ||
util.destroy(socket, err) | ||
} | ||
if (socket.destroyed) { | ||
return | ||
} | ||
if (bytesWritten === 0) { | ||
if (expectsPayload) { | ||
// https://tools.ietf.org/html/rfc7230#section-3.3.2 | ||
// A user agent SHOULD send a Content-Length in a request message when | ||
// no Transfer-Encoding is sent and the request method defines a meaning | ||
// for an enclosed payload body. | ||
socket.write(`${header}content-length: 0\r\n\r\n\r\n`, 'ascii') | ||
} else { | ||
util.destroy(body) | ||
socket.write(`${header}\r\n`, 'ascii') | ||
} | ||
} else if (contentLength === null) { | ||
socket.write('\r\n0\r\n\r\n', 'ascii') | ||
} | ||
if (err) { | ||
assert(client[kRunning] <= 1, 'pipeline should only contain this request') | ||
util.destroy(socket, err) | ||
// TODO (fix): Add comment clarifying what this does? | ||
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { | ||
// istanbul ignore else: only for jest | ||
if (socket[kParser].timeout.refresh) { | ||
socket[kParser].timeout.refresh() | ||
} | ||
} | ||
if (socket.destroyed) { | ||
return | ||
resume(client) | ||
} | ||
body | ||
.on('data', onData) | ||
.on('end', onFinished) | ||
.on('error', onFinished) | ||
.on('close', onAbort) | ||
socket | ||
.on('drain', onDrain) | ||
.on('error', onFinished) | ||
} | ||
async function writeIterable ({ client, request, socket, contentLength, header, expectsPayload }) { | ||
const { body } = request | ||
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') | ||
let callback = noop | ||
function onDrain () { | ||
const cb = callback | ||
callback = noop | ||
cb() | ||
} | ||
const waitForDrain = () => new Promise((resolve) => { | ||
/* istanbul ignore next: assertion */ | ||
assert(callback === noop) | ||
/* istanbul ignore else: only for Node 12 */ | ||
if (!socket[kError]) { | ||
callback = resolve | ||
} else { | ||
resolve() | ||
} | ||
}) | ||
socket[kWriting] = true | ||
socket | ||
.on('close', onDrain) | ||
.on('drain', onDrain) | ||
let bytesWritten = 0 | ||
try { | ||
// TODO (fix): What if socket errors while waiting for body? | ||
// It's up to the user to somehow abort the async iterable. | ||
for await (const chunk of body) { | ||
if (socket[kError]) { | ||
throw socket[kError] | ||
} | ||
const len = Buffer.byteLength(chunk) | ||
if (!len) { | ||
continue | ||
} | ||
// TODO: What if not ended and bytesWritten === contentLength? | ||
// We should defer writing chunks. | ||
if (contentLength !== null && bytesWritten + len > contentLength) { | ||
if (client[kStrictContentLength]) { | ||
throw new RequestContentLengthMismatchError() | ||
} | ||
process.emitWarning(new RequestContentLengthMismatchError()) | ||
} | ||
if (bytesWritten === 0) { | ||
if (expectsPayload) { | ||
// https://tools.ietf.org/html/rfc7230#section-3.3.2 | ||
// A user agent SHOULD send a Content-Length in a request message when | ||
// no Transfer-Encoding is sent and the request method defines a meaning | ||
// for an enclosed payload body. | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
socket.write(`${header}content-length: 0\r\n\r\n\r\n`, 'ascii') | ||
if (contentLength === null) { | ||
socket.write(`${header}transfer-encoding: chunked\r\n`, 'ascii') | ||
} else { | ||
socket.write(`${header}\r\n`, 'ascii') | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') | ||
} | ||
} else if (contentLength === null) { | ||
socket.write('\r\n0\r\n\r\n', 'ascii') | ||
} | ||
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { | ||
// istanbul ignore else: only for jest | ||
if (socket[kParser].timeout.refresh) { | ||
socket[kParser].timeout.refresh() | ||
} | ||
if (contentLength === null) { | ||
socket.write(`\r\n${len.toString(16)}\r\n`, 'ascii') | ||
} | ||
resume(client) | ||
bytesWritten += len | ||
if (!socket.write(chunk)) { | ||
await waitForDrain() | ||
} | ||
if (socket[kError]) { | ||
throw socket[kError] | ||
} | ||
} | ||
body | ||
.on('data', onData) | ||
.on('end', onFinished) | ||
.on('error', onFinished) | ||
.on('close', onAbort) | ||
if (socket[kError]) { | ||
throw socket[kError] | ||
} | ||
if (bytesWritten === 0) { | ||
if (expectsPayload) { | ||
// https://tools.ietf.org/html/rfc7230#section-3.3.2 | ||
// A user agent SHOULD send a Content-Length in a request message when | ||
// no Transfer-Encoding is sent and the request method defines a meaning | ||
// for an enclosed payload body. | ||
socket.write(`${header}content-length: 0\r\n\r\n\r\n`, 'ascii') | ||
} else { | ||
socket.write(`${header}\r\n`, 'ascii') | ||
} | ||
} else if (contentLength === null) { | ||
socket.write('\r\n0\r\n\r\n', 'ascii') | ||
} | ||
if (contentLength !== null && bytesWritten !== contentLength) { | ||
if (client[kStrictContentLength]) { | ||
throw new RequestContentLengthMismatchError() | ||
} else { | ||
process.emitWarning(new RequestContentLengthMismatchError()) | ||
} | ||
} | ||
// TODO (fix): Add comment clarifying what this does? | ||
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { | ||
// istanbul ignore else: only for jest | ||
if (socket[kParser].timeout.refresh) { | ||
socket[kParser].timeout.refresh() | ||
} | ||
} | ||
resume(client) | ||
} catch (err) { | ||
assert(client[kRunning] <= 1, 'pipeline should only contain this request') | ||
util.destroy(socket, err) | ||
} finally { | ||
socket[kWriting] = false | ||
socket | ||
.on('drain', onDrain) | ||
.on('error', onFinished) | ||
.off('close', onDrain) | ||
.off('drain', onDrain) | ||
} | ||
return true | ||
} | ||
@@ -1563,0 +1735,0 @@ |
@@ -59,4 +59,6 @@ 'use strict' | ||
this.body = body.length ? Buffer.from(body) : null | ||
} else if (util.isIterable(body)) { | ||
this.body = body | ||
} else { | ||
throw new InvalidArgumentError('body must be a string, a Buffer or a Readable stream') | ||
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') | ||
} | ||
@@ -134,3 +136,3 @@ | ||
return this[kHandler].onConnect(abort, this.context) | ||
return this[kHandler].onConnect(abort) | ||
} | ||
@@ -137,0 +139,0 @@ |
@@ -40,3 +40,4 @@ module.exports = { | ||
kConnector: Symbol('connector'), | ||
kStrictContentLength: Symbol('strict content length') | ||
kStrictContentLength: Symbol('strict content length'), | ||
kMaxRedirections: Symbol('maxRedirections') | ||
} |
@@ -110,2 +110,10 @@ 'use strict' | ||
function isIterable (obj) { | ||
return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) | ||
} | ||
function isStrictIterable (obj) { | ||
return obj && !isStream(obj) && typeof obj !== 'string' && !isBuffer(obj) && isIterable(obj) | ||
} | ||
function bodyLength (body) { | ||
@@ -117,2 +125,4 @@ if (body && typeof body.on === 'function') { | ||
: null | ||
} else if (isStrictIterable(body)) { | ||
return null | ||
} | ||
@@ -186,2 +196,4 @@ | ||
isReadable, | ||
isIterable, | ||
isStrictIterable, | ||
isDestroyed, | ||
@@ -188,0 +200,0 @@ parseHeaders, |
'use strict' | ||
const { InvalidArgumentError } = require('../core/errors') | ||
const util = require('../core/util') | ||
const assert = require('assert') | ||
@@ -9,3 +9,3 @@ const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] | ||
class RedirectHandler { | ||
constructor (dispatcher, { maxRedirections, ...opts }, handler) { | ||
constructor (dispatcher, maxRedirections, opts, handler) { | ||
this.dispatcher = dispatcher | ||
@@ -20,7 +20,5 @@ this.location = null | ||
onConnect (abort, context = {}) { | ||
context.history = this.history | ||
onConnect (abort) { | ||
this.abort = abort | ||
this.handler.onConnect(abort, context) | ||
this.handler.onConnect(abort, { history: this.history }) | ||
} | ||
@@ -50,2 +48,4 @@ | ||
this.opts = { ...this.opts } | ||
// Remove headers referring to the original URL. | ||
@@ -57,2 +57,3 @@ // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. | ||
this.opts.origin = origin | ||
this.opts.maxRedirections = 0 | ||
@@ -149,4 +150,4 @@ // https://tools.ietf.org/html/rfc7231#section-6.4.4 | ||
} | ||
} else if (headers != null) { | ||
throw new InvalidArgumentError('headers must be an object or an array') | ||
} else { | ||
assert(headers == null, 'headers must be an object or an array') | ||
} | ||
@@ -153,0 +154,0 @@ return ret |
{ | ||
"name": "undici", | ||
"version": "4.0.0", | ||
"version": "4.1.0", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -55,6 +55,6 @@ "homepage": "https://undici.nodejs.org", | ||
"cronometro": "^0.8.0", | ||
"docsify-cli": "^4.4.3", | ||
"docsify-cli": "^4.4.2", | ||
"https-pem": "^2.0.0", | ||
"husky": "^6.0.0", | ||
"jest": "^26.6.3", | ||
"jest": "^27.0.5", | ||
"jsfuzz": "^1.0.15", | ||
@@ -65,3 +65,3 @@ "pre-commit": "^1.2.2", | ||
"semver": "^7.3.5", | ||
"sinon": "^10.0.0", | ||
"sinon": "^11.1.1", | ||
"snazzy": "^9.0.0", | ||
@@ -68,0 +68,0 @@ "standard": "^16.0.3", |
@@ -29,6 +29,6 @@ import { URL } from 'url' | ||
upgrade(options: Dispatcher.UpgradeOptions, callback: (err: Error | null, data: Dispatcher.UpgradeData) => void): void; | ||
/** Closes the client and gracefully waits for enqueued requests to complete before invoking the callback (or returnning a promise if no callback is provided). */ | ||
/** Closes the client and gracefully waits for enqueued requests to complete before invoking the callback (or returning a promise if no callback is provided). */ | ||
close(): Promise<void>; | ||
close(callback: () => void): void; | ||
/** Destroy the client abruptly with the given err. All the pending and running requests will be asynchronously aborted and error. Waits until socket is closed before invoking the callback (or returnning a promise if no callback is provided). Since this operation is asynchronously dispatched there might still be some progress on dispatched requests. */ | ||
/** Destroy the client abruptly with the given err. All the pending and running requests will be asynchronously aborted and error. Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided). Since this operation is asynchronously dispatched there might still be some progress on dispatched requests. */ | ||
destroy(): Promise<void>; | ||
@@ -48,4 +48,4 @@ destroy(err: Error | null): Promise<void>; | ||
/** Default: `null` */ | ||
headers?: IncomingHttpHeaders | null; | ||
/** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceeding requests in the pipeline has completed. Default: `true` if `method` is `HEAD` or `GET`. */ | ||
headers?: IncomingHttpHeaders | string[] | null; | ||
/** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */ | ||
idempotent?: boolean; | ||
@@ -58,3 +58,3 @@ /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */ | ||
/** Default: `null` */ | ||
headers?: IncomingHttpHeaders | null; | ||
headers?: IncomingHttpHeaders | string[] | null; | ||
/** Default: `null` */ | ||
@@ -80,3 +80,3 @@ signal?: AbortSignal | EventEmitter | null; | ||
/** Default: `null` */ | ||
headers?: IncomingHttpHeaders | null; | ||
headers?: IncomingHttpHeaders | string[] | null; | ||
/** A string of comma separated protocols, in descending preference order. Default: `'Websocket'` */ | ||
@@ -83,0 +83,0 @@ protocol?: string; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
426806
5046