Comparing version 2.1.1 to 2.2.0
@@ -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
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
385301
83
12311
13