Comparing version
@@ -555,3 +555,3 @@ 'use strict' | ||
try { | ||
if (request.onHeaders(statusCode, headers, statusCode < 200 ? null : socket[kResume]) === false) { | ||
if (request.onHeaders(statusCode, headers, socket[kResume]) === false) { | ||
socket[kPause]() | ||
@@ -558,0 +558,0 @@ } |
@@ -16,2 +16,6 @@ 'use strict' | ||
// Borrowed from https://gist.github.com/dperini/729294 | ||
// eslint-disable-next-line no-control-regex | ||
const REGEXP_ABSOLUTE_URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x00a1-\xffff0-9]+-?)*[a-z\x00a1-\xffff0-9]+)(?:\.(?:[a-z\x00a1-\xffff0-9]+-?)*[a-z\x00a1-\xffff0-9]+)*(?:\.(?:[a-z\x00a1-\xffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/ius | ||
class Request { | ||
@@ -27,4 +31,6 @@ constructor ({ | ||
}, handler) { | ||
if (typeof path !== 'string' || path[0] !== '/') { | ||
throw new InvalidArgumentError('path must be a valid path') | ||
if (typeof path !== 'string') { | ||
throw new InvalidArgumentError('path must be a string') | ||
} else if (path[0] !== '/' && !REGEXP_ABSOLUTE_URL.test(path)) { | ||
throw new InvalidArgumentError('path must be an absolute URL or start with a slash') | ||
} | ||
@@ -31,0 +37,0 @@ |
@@ -51,3 +51,3 @@ 'use strict' | ||
const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)s/ | ||
const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ | ||
function parseKeepAliveTimeout (val) { | ||
@@ -54,0 +54,0 @@ const m = val.match(KEEPALIVE_TIMEOUT_EXPR) |
{ | ||
"name": "undici", | ||
"version": "2.1.1", | ||
"version": "2.2.0", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"module": "wrapper.mjs", | ||
"scripts": { | ||
"lint": "standard | snazzy", | ||
"test": "tap test/*.js --no-coverage && jest test/jest/test", | ||
"test": "tap test/*.{mjs,js} --no-coverage && jest test/jest/test", | ||
"test:typescript": "tsd", | ||
"coverage": "standard | snazzy && tap test/*.js", | ||
@@ -32,2 +34,3 @@ "bench": "npx concurrently -k -s first \"node benchmarks/server.js\" \"node -e 'setTimeout(() => {}, 1000)' && node benchmarks\"" | ||
"@sinonjs/fake-timers": "^6.0.1", | ||
"@types/node": "^14.6.2", | ||
"abort-controller": "^3.0.0", | ||
@@ -42,7 +45,14 @@ "benchmark": "^2.1.4", | ||
"standard": "^14.3.4", | ||
"tap": "^14.10.8" | ||
"tap": "^14.10.8", | ||
"tsd": "^0.13.1" | ||
}, | ||
"pre-commit": [ | ||
"coverage" | ||
] | ||
], | ||
"tsd": { | ||
"directory": "test/types", | ||
"compilerOptions": { | ||
"esModuleInterop": true | ||
} | ||
} | ||
} |
@@ -105,3 +105,3 @@ # undici | ||
Default: `null`. | ||
* `signal: AbortController|EventEmitter|Null` | ||
* `signal: AbortSignal|EventEmitter|Null` | ||
Default: `null`. | ||
@@ -197,6 +197,6 @@ * `requestTimeout: Number`, the timeout after which a request will time out, in | ||
A request can may be aborted using either an `AbortController` or an `EventEmitter`. | ||
To use `AbortController`, you will need to `npm i abort-controller`. | ||
To use `AbortController` in Node.js versions earlier than 15, you will need to | ||
install a shim - `npm i abort-controller`. | ||
```js | ||
const { AbortController } = require('abort-controller') | ||
const { Client } = require('undici') | ||
@@ -390,3 +390,3 @@ | ||
Default: `null` | ||
* `signal: AbortController|EventEmitter|Null`. | ||
* `signal: AbortSignal|EventEmitter|Null`. | ||
Default: `null` | ||
@@ -419,3 +419,3 @@ * `requestTimeout: Number`, the timeout after which a request will time out, in | ||
Default: `null` | ||
* `signal: AbortController|EventEmitter|Null`. | ||
* `signal: AbortSignal|EventEmitter|Null`. | ||
Default: `null` | ||
@@ -437,3 +437,3 @@ * `requestTimeout: Number`, the timeout after which a request will time out, in | ||
<a name='dispatch'></a> | ||
#### `client.dispatch(opts, handler): Promise|Void` | ||
#### `client.dispatch(opts, handler): Void` | ||
@@ -490,3 +490,3 @@ This is the low level API which all the preceeding APIs are implemented on top of. | ||
Closes the client and gracefully waits fo enqueued requests to | ||
Closes the client and gracefully waits for enqueued requests to | ||
complete before invoking the callback. | ||
@@ -493,0 +493,0 @@ |
'use strict' | ||
const { test } = require('tap') | ||
const { AbortController } = require('abort-controller') | ||
const { AbortController: NPMAbortController } = require('abort-controller') | ||
const { Client, errors } = require('..') | ||
@@ -9,114 +9,55 @@ const { createServer } = require('http') | ||
test('Abort before sending request (no body)', (t) => { | ||
t.plan(3) | ||
const controllers = [{ | ||
AbortControllerImpl: NPMAbortController, | ||
controllerName: 'npm-abortcontroller-shim' | ||
}] | ||
let count = 0 | ||
const server = createServer((req, res) => { | ||
if (count === 1) { | ||
t.fail('The second request should never be executed') | ||
} | ||
count += 1 | ||
res.end('hello') | ||
if (global.AbortController) { | ||
controllers.push({ | ||
AbortControllerImpl: global.AbortController, | ||
controllerName: 'native-abortcontroller' | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
} | ||
for (const { AbortControllerImpl, controllerName } of controllers) { | ||
test(`Abort ${controllerName} before sending request (no body)`, (t) => { | ||
t.plan(3) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
const abortController = new AbortController() | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ path: '/', method: 'GET' }, (err, response) => { | ||
t.error(err) | ||
const bufs = [] | ||
response.body.on('data', (buf) => { | ||
bufs.push(buf) | ||
}) | ||
response.body.on('end', () => { | ||
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) | ||
}) | ||
let count = 0 | ||
const server = createServer((req, res) => { | ||
if (count === 1) { | ||
t.fail('The second request should never be executed') | ||
} | ||
count += 1 | ||
res.end('hello') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
}) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
const abortController = new AbortControllerImpl() | ||
t.teardown(client.destroy.bind(client)) | ||
abortController.abort() | ||
}) | ||
}) | ||
client.request({ path: '/', method: 'GET' }, (err, response) => { | ||
t.error(err) | ||
const bufs = [] | ||
response.body.on('data', (buf) => { | ||
bufs.push(buf) | ||
}) | ||
response.body.on('end', () => { | ||
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) | ||
}) | ||
}) | ||
test('Abort while waiting response (no body)', (t) => { | ||
t.plan(1) | ||
const abortController = new AbortController() | ||
const server = createServer((req, res) => { | ||
abortController.abort() | ||
res.setHeader('content-type', 'text/plain') | ||
res.end('hello world') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
}) | ||
}) | ||
}) | ||
test('Abort while waiting response (write headers started) (no body)', (t) => { | ||
t.plan(1) | ||
const abortController = new AbortController() | ||
const server = createServer((req, res) => { | ||
res.writeHead(200, { 'content-type': 'text/plain' }) | ||
res.flushHeaders() | ||
abortController.abort() | ||
res.end('hello world') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
}) | ||
}) | ||
}) | ||
test('Abort while waiting response (write headers and write body started) (no body)', (t) => { | ||
t.plan(2) | ||
const abortController = new AbortController() | ||
const server = createServer((req, res) => { | ||
res.writeHead(200, { 'content-type': 'text/plain' }) | ||
res.write('hello') | ||
res.end('world') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.error(err) | ||
response.body.on('data', () => { | ||
abortController.abort() | ||
}) | ||
response.body.on('error', err => { | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
}) | ||
abortController.abort() | ||
}) | ||
}) | ||
}) | ||
function waitingWithBody (body, type) { | ||
test(`Abort while waiting response (with body ${type})`, (t) => { | ||
test(`Abort ${controllerName} while waiting response (no body)`, (t) => { | ||
t.plan(1) | ||
const abortController = new AbortController() | ||
const abortController = new AbortControllerImpl() | ||
const server = createServer((req, res) => { | ||
@@ -133,3 +74,3 @@ abortController.abort() | ||
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => { | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
@@ -139,13 +80,7 @@ }) | ||
}) | ||
} | ||
waitingWithBody('hello', 'string') | ||
waitingWithBody(createReadStream(__filename), 'stream') | ||
waitingWithBody(new Uint8Array([42]), 'Uint8Array') | ||
function writeHeadersStartedWithBody (body, type) { | ||
test(`Abort while waiting response (write headers started) (with body ${type})`, (t) => { | ||
test(`Abort ${controllerName} while waiting response (write headers started) (no body)`, (t) => { | ||
t.plan(1) | ||
const abortController = new AbortController() | ||
const abortController = new AbortControllerImpl() | ||
const server = createServer((req, res) => { | ||
@@ -163,3 +98,3 @@ res.writeHead(200, { 'content-type': 'text/plain' }) | ||
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => { | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
@@ -169,13 +104,7 @@ }) | ||
}) | ||
} | ||
writeHeadersStartedWithBody('hello', 'string') | ||
writeHeadersStartedWithBody(createReadStream(__filename), 'stream') | ||
writeHeadersStartedWithBody(new Uint8Array([42]), 'Uint8Array') | ||
function writeBodyStartedWithBody (body, type) { | ||
test(`Abort while waiting response (write headers and write body started) (with body ${type})`, (t) => { | ||
test(`Abort ${controllerName} while waiting response (write headers and write body started) (no body)`, (t) => { | ||
t.plan(2) | ||
const abortController = new AbortController() | ||
const abortController = new AbortControllerImpl() | ||
const server = createServer((req, res) => { | ||
@@ -192,3 +121,3 @@ res.writeHead(200, { 'content-type': 'text/plain' }) | ||
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => { | ||
client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { | ||
t.error(err) | ||
@@ -204,6 +133,90 @@ response.body.on('data', () => { | ||
}) | ||
function waitingWithBody (body, type) { // eslint-disable-line | ||
test(`Abort ${controllerName} while waiting response (with body ${type})`, (t) => { | ||
t.plan(1) | ||
const abortController = new AbortControllerImpl() | ||
const server = createServer((req, res) => { | ||
abortController.abort() | ||
res.setHeader('content-type', 'text/plain') | ||
res.end('hello world') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
}) | ||
}) | ||
}) | ||
} | ||
waitingWithBody('hello', 'string') | ||
waitingWithBody(createReadStream(__filename), 'stream') | ||
waitingWithBody(new Uint8Array([42]), 'Uint8Array') | ||
function writeHeadersStartedWithBody (body, type) { // eslint-disable-line | ||
test(`Abort ${controllerName} while waiting response (write headers started) (with body ${type})`, (t) => { | ||
t.plan(1) | ||
const abortController = new AbortControllerImpl() | ||
const server = createServer((req, res) => { | ||
res.writeHead(200, { 'content-type': 'text/plain' }) | ||
res.flushHeaders() | ||
abortController.abort() | ||
res.end('hello world') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
}) | ||
}) | ||
}) | ||
} | ||
writeHeadersStartedWithBody('hello', 'string') | ||
writeHeadersStartedWithBody(createReadStream(__filename), 'stream') | ||
writeHeadersStartedWithBody(new Uint8Array([42]), 'Uint8Array') | ||
function writeBodyStartedWithBody (body, type) { // eslint-disable-line | ||
test(`Abort ${controllerName} while waiting response (write headers and write body started) (with body ${type})`, (t) => { | ||
t.plan(2) | ||
const abortController = new AbortControllerImpl() | ||
const server = createServer((req, res) => { | ||
res.writeHead(200, { 'content-type': 'text/plain' }) | ||
res.write('hello') | ||
res.end('world') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => { | ||
t.error(err) | ||
response.body.on('data', () => { | ||
abortController.abort() | ||
}) | ||
response.body.on('error', err => { | ||
t.ok(err instanceof errors.RequestAbortedError) | ||
}) | ||
}) | ||
}) | ||
}) | ||
} | ||
writeBodyStartedWithBody('hello', 'string') | ||
writeBodyStartedWithBody(createReadStream(__filename), 'stream') | ||
writeBodyStartedWithBody(new Uint8Array([42]), 'Uint8Array') | ||
} | ||
writeBodyStartedWithBody('hello', 'string') | ||
writeBodyStartedWithBody(createReadStream(__filename), 'stream') | ||
writeBodyStartedWithBody(new Uint8Array([42]), 'Uint8Array') |
@@ -77,2 +77,36 @@ 'use strict' | ||
test('keep-alive header no postfix', (t) => { | ||
t.plan(2) | ||
const server = createServer((socket) => { | ||
socket.write('HTTP/1.1 200 OK\r\n') | ||
socket.write('Content-Length: 0\r\n') | ||
socket.write('Keep-Alive: timeout=2\r\n') | ||
socket.write('Connection: keep-alive\r\n') | ||
socket.write('\r\n\r\n') | ||
}) | ||
t.teardown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client(`http://localhost:${server.address().port}`) | ||
t.teardown(client.destroy.bind(client)) | ||
client.request({ | ||
path: '/', | ||
method: 'GET' | ||
}, (err, { body }) => { | ||
t.error(err) | ||
body.on('end', () => { | ||
const timeout = setTimeout(() => { | ||
t.fail() | ||
}, 2e3) | ||
client.on('disconnect', () => { | ||
t.pass() | ||
clearTimeout(timeout) | ||
}) | ||
}).resume() | ||
}) | ||
}) | ||
}) | ||
test('keep-alive not timeout', (t) => { | ||
@@ -79,0 +113,0 @@ t.plan(2) |
@@ -628,2 +628,28 @@ 'use strict' | ||
test('an absolute url as path', (t) => { | ||
t.plan(2) | ||
const path = 'http://example.com' | ||
const server = createServer((req, res) => { | ||
t.strictEqual(req.url, path) | ||
res.end() | ||
}) | ||
t.tearDown(server.close.bind(server)) | ||
server.listen(0, () => { | ||
const client = new Client({ | ||
hostname: 'localhost', | ||
port: server.address().port, | ||
protocol: 'http' | ||
}) | ||
t.tearDown(client.close.bind(client)) | ||
client.request({ path, method: 'GET' }, (err, data) => { | ||
t.error(err) | ||
data.body.resume() | ||
}) | ||
}) | ||
}) | ||
test('multiple destroy callback', (t) => { | ||
@@ -630,0 +656,0 @@ t.plan(4) |
@@ -26,3 +26,3 @@ 'use strict' | ||
t.ok(err instanceof errors.InvalidArgumentError) | ||
t.strictEqual(err.message, 'path must be a valid path') | ||
t.strictEqual(err.message, 'path must be a string') | ||
}) | ||
@@ -32,3 +32,3 @@ | ||
t.ok(err instanceof errors.InvalidArgumentError) | ||
t.strictEqual(err.message, 'path must be a valid path') | ||
t.strictEqual(err.message, 'path must be an absolute URL or start with a slash') | ||
}) | ||
@@ -35,0 +35,0 @@ }) |
Sorry, the diff of this file is not supported yet
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
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
385301
7.05%83
10.67%12311
4.45%13
18.18%