mirror of
https://github.com/tenrok/axios.git
synced 2026-06-20 20:00:40 +03:00
feat(http): add HTTP2 support; (#7150)
This commit is contained in:
@@ -87,6 +87,7 @@
|
|||||||
- [🔥 Custom fetch](#-custom-fetch)
|
- [🔥 Custom fetch](#-custom-fetch)
|
||||||
- [🔥 Using with Tauri](#-using-with-tauri)
|
- [🔥 Using with Tauri](#-using-with-tauri)
|
||||||
- [🔥 Using with SvelteKit](#-using-with-sveltekit-)
|
- [🔥 Using with SvelteKit](#-using-with-sveltekit-)
|
||||||
|
- [🔥 HTTP2](#-http2)
|
||||||
- [Semver](#semver)
|
- [Semver](#semver)
|
||||||
- [Promises](#promises)
|
- [Promises](#promises)
|
||||||
- [TypeScript](#typescript)
|
- [TypeScript](#typescript)
|
||||||
@@ -1702,6 +1703,34 @@ export async function load({ fetch }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🔥 HTTP2
|
||||||
|
|
||||||
|
In version `1.13.0`, experimental `HTTP2` support was added to the `http` adapter.
|
||||||
|
The `httpVersion` option is now available to select the protocol version used.
|
||||||
|
Additional native options for the internal `session.request()` call can be passed via the `http2Options` config.
|
||||||
|
This config also includes the custom `sessionTimeout` parameter, which defaults to `1000ms`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const form = new FormData();
|
||||||
|
|
||||||
|
form.append('foo', '123');
|
||||||
|
|
||||||
|
const {data, headers, status} = await axios.post('https://httpbin.org/post', form, {
|
||||||
|
httpVersion: 2,
|
||||||
|
http2Options: {
|
||||||
|
// rejectUnauthorized: false,
|
||||||
|
// sessionTimeout: 1000
|
||||||
|
},
|
||||||
|
onUploadProgress(e) {
|
||||||
|
console.log('upload progress', e);
|
||||||
|
},
|
||||||
|
onDownloadProgress(e) {
|
||||||
|
console.log('download progress', e);
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Semver
|
## Semver
|
||||||
|
|
||||||
Since Axios has reached a `v.1.0.0` we will fully embrace semver as per the spec [here](https://semver.org/)
|
Since Axios has reached a `v.1.0.0` we will fully embrace semver as per the spec [here](https://semver.org/)
|
||||||
|
|||||||
@@ -436,6 +436,10 @@ declare namespace axios {
|
|||||||
((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>);
|
((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>);
|
||||||
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
|
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
|
||||||
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'> | Record<string, any>;
|
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'> | Record<string, any>;
|
||||||
|
httpVersion?: 1 | 2;
|
||||||
|
http2Options?: Record<string, any> & {
|
||||||
|
sessionTimeout?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias
|
// Alias
|
||||||
|
|||||||
Vendored
+4
@@ -369,6 +369,10 @@ export interface AxiosRequestConfig<D = any> {
|
|||||||
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
|
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
|
||||||
parseReviver?: (this: any, key: string, value: any) => any;
|
parseReviver?: (this: any, key: string, value: any) => any;
|
||||||
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'> | Record<string, any>;
|
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'> | Record<string, any>;
|
||||||
|
httpVersion?: 1 | 2;
|
||||||
|
http2Options?: Record<string, any> & {
|
||||||
|
sessionTimeout?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias
|
// Alias
|
||||||
|
|||||||
+188
-46
@@ -1,5 +1,4 @@
|
|||||||
'use strict';
|
import { connect, constants } from 'http2';
|
||||||
|
|
||||||
import utils from './../utils.js';
|
import utils from './../utils.js';
|
||||||
import settle from './../core/settle.js';
|
import settle from './../core/settle.js';
|
||||||
import buildFullPath from '../core/buildFullPath.js';
|
import buildFullPath from '../core/buildFullPath.js';
|
||||||
@@ -37,6 +36,13 @@ const brotliOptions = {
|
|||||||
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
|
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
HTTP2_HEADER_SCHEME,
|
||||||
|
HTTP2_HEADER_METHOD,
|
||||||
|
HTTP2_HEADER_PATH,
|
||||||
|
HTTP2_HEADER_STATUS
|
||||||
|
} = constants;
|
||||||
|
|
||||||
const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress);
|
const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress);
|
||||||
|
|
||||||
const {http: httpFollow, https: httpsFollow} = followRedirects;
|
const {http: httpFollow, https: httpsFollow} = followRedirects;
|
||||||
@@ -56,6 +62,66 @@ const flushOnFinish = (stream, [throttled, flush]) => {
|
|||||||
return throttled;
|
return throttled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Http2Sessions {
|
||||||
|
constructor() {
|
||||||
|
this.sessions = Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSession(authority, options) {
|
||||||
|
options = Object.assign({
|
||||||
|
sessionTimeout: 1000
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
let authoritySessions;
|
||||||
|
|
||||||
|
if ((authoritySessions = this.sessions[authority])) {
|
||||||
|
let len = authoritySessions.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const [sessionHandle, sessionOptions] = authoritySessions[i];
|
||||||
|
if (!sessionHandle.destroyed && !sessionHandle.closed && util.isDeepStrictEqual(sessionOptions, options)) {
|
||||||
|
return sessionHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = connect(authority, options);
|
||||||
|
|
||||||
|
session.once('close', () => {
|
||||||
|
let entries = authoritySessions, len = entries.length, i = len;
|
||||||
|
|
||||||
|
while (i--) {
|
||||||
|
if (entries[i][0] === session) {
|
||||||
|
entries.splice(i, 1);
|
||||||
|
if (len === 1) {
|
||||||
|
delete this.sessions[authority];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Http2Sessions.setTimeout(session, options.sessionTimeout);
|
||||||
|
|
||||||
|
let entries = this.sessions[authority], entry = [
|
||||||
|
session,
|
||||||
|
options
|
||||||
|
];
|
||||||
|
|
||||||
|
entries ? this.sessions[authority].push(entry) : authoritySessions = this.sessions[authority] = [entry];
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setTimeout(session, timeout = 1000) {
|
||||||
|
session && session.setTimeout(timeout, () => {
|
||||||
|
session.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const http2Sessions = new Http2Sessions();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the proxy or config beforeRedirects functions are defined, call them with the options
|
* If the proxy or config beforeRedirects functions are defined, call them with the options
|
||||||
@@ -168,16 +234,66 @@ const resolveFamily = ({address, family}) => {
|
|||||||
|
|
||||||
const buildAddressEntry = (address, family) => resolveFamily(utils.isObject(address) ? address : {address, family});
|
const buildAddressEntry = (address, family) => resolveFamily(utils.isObject(address) ? address : {address, family});
|
||||||
|
|
||||||
|
const http2Transport = {
|
||||||
|
request(options, cb) {
|
||||||
|
const authority = options.protocol + '//' + options.hostname + ':' + (options.port || 80);
|
||||||
|
|
||||||
|
const {http2Options, headers} = options;
|
||||||
|
|
||||||
|
const session = http2Sessions.getSession(authority, http2Options);
|
||||||
|
|
||||||
|
const http2Headers = {
|
||||||
|
[HTTP2_HEADER_SCHEME]: options.protocol.replace(':', ''),
|
||||||
|
[HTTP2_HEADER_METHOD]: options.method,
|
||||||
|
[HTTP2_HEADER_PATH]: options.path,
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.forEach(headers, (header, name) => {
|
||||||
|
name.charAt(0) !== ':' && (http2Headers[name] = header);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = session.request(http2Headers);
|
||||||
|
|
||||||
|
req.once('response', (responseHeaders) => {
|
||||||
|
const response = req; //duplex
|
||||||
|
|
||||||
|
responseHeaders = Object.assign({}, responseHeaders);
|
||||||
|
|
||||||
|
const status = responseHeaders[HTTP2_HEADER_STATUS];
|
||||||
|
|
||||||
|
delete responseHeaders[HTTP2_HEADER_STATUS];
|
||||||
|
|
||||||
|
response.headers = responseHeaders;
|
||||||
|
|
||||||
|
response.statusCode = +status;
|
||||||
|
|
||||||
|
cb(response);
|
||||||
|
})
|
||||||
|
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*eslint consistent-return:0*/
|
/*eslint consistent-return:0*/
|
||||||
export default isHttpAdapterSupported && function httpAdapter(config) {
|
export default isHttpAdapterSupported && function httpAdapter(config) {
|
||||||
return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) {
|
return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) {
|
||||||
let {data, lookup, family} = config;
|
let {data, lookup, family, httpVersion = 1, http2Options} = config;
|
||||||
const {responseType, responseEncoding} = config;
|
const {responseType, responseEncoding} = config;
|
||||||
const method = config.method.toUpperCase();
|
const method = config.method.toUpperCase();
|
||||||
let isDone;
|
let isDone;
|
||||||
let rejected = false;
|
let rejected = false;
|
||||||
let req;
|
let req;
|
||||||
|
|
||||||
|
httpVersion = Number(httpVersion);
|
||||||
|
if (Number.isNaN(httpVersion)) {
|
||||||
|
throw TypeError(`Invalid protocol version: '${config.httpVersion}' is not a number`);
|
||||||
|
}
|
||||||
|
if (httpVersion !== 1 && httpVersion !== 2) {
|
||||||
|
throw TypeError(`Unsupported protocol version '${httpVersion}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHttp2 = httpVersion === 2;
|
||||||
|
|
||||||
if (lookup) {
|
if (lookup) {
|
||||||
const _lookup = callbackify(lookup, (value) => utils.isArray(value) ? value : [value]);
|
const _lookup = callbackify(lookup, (value) => utils.isArray(value) ? value : [value]);
|
||||||
// hotfix to support opt.all option which is required for node 20.x
|
// hotfix to support opt.all option which is required for node 20.x
|
||||||
@@ -194,8 +310,17 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// temporary internal emitter until the AxiosRequest class will be implemented
|
const abortEmitter = new EventEmitter();
|
||||||
const emitter = new EventEmitter();
|
|
||||||
|
function abort(reason) {
|
||||||
|
try {
|
||||||
|
abortEmitter.emit('abort', !reason || reason.type ? new CanceledError(null, config, req) : reason);
|
||||||
|
} catch(err) {
|
||||||
|
console.warn('emit error', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abortEmitter.once('abort', reject);
|
||||||
|
|
||||||
const onFinished = () => {
|
const onFinished = () => {
|
||||||
if (config.cancelToken) {
|
if (config.cancelToken) {
|
||||||
@@ -206,23 +331,9 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
config.signal.removeEventListener('abort', abort);
|
config.signal.removeEventListener('abort', abort);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.removeAllListeners();
|
abortEmitter.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDone((value, isRejected) => {
|
|
||||||
isDone = true;
|
|
||||||
if (isRejected) {
|
|
||||||
rejected = true;
|
|
||||||
onFinished();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function abort(reason) {
|
|
||||||
emitter.emit('abort', !reason || reason.type ? new CanceledError(null, config, req) : reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitter.once('abort', reject);
|
|
||||||
|
|
||||||
if (config.cancelToken || config.signal) {
|
if (config.cancelToken || config.signal) {
|
||||||
config.cancelToken && config.cancelToken.subscribe(abort);
|
config.cancelToken && config.cancelToken.subscribe(abort);
|
||||||
if (config.signal) {
|
if (config.signal) {
|
||||||
@@ -230,6 +341,31 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDone((response, isRejected) => {
|
||||||
|
isDone = true;
|
||||||
|
|
||||||
|
if (isRejected) {
|
||||||
|
rejected = true;
|
||||||
|
onFinished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = response;
|
||||||
|
|
||||||
|
if (data instanceof stream.Readable || data instanceof stream.Duplex) {
|
||||||
|
const offListeners = stream.finished(data, () => {
|
||||||
|
offListeners();
|
||||||
|
onFinished();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onFinished();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Parse url
|
// Parse url
|
||||||
const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
||||||
const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined);
|
const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined);
|
||||||
@@ -436,7 +572,8 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
protocol,
|
protocol,
|
||||||
family,
|
family,
|
||||||
beforeRedirect: dispatchBeforeRedirect,
|
beforeRedirect: dispatchBeforeRedirect,
|
||||||
beforeRedirects: {}
|
beforeRedirects: {},
|
||||||
|
http2Options
|
||||||
};
|
};
|
||||||
|
|
||||||
// cacheable-lookup integration hotfix
|
// cacheable-lookup integration hotfix
|
||||||
@@ -453,18 +590,23 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
let transport;
|
let transport;
|
||||||
const isHttpsRequest = isHttps.test(options.protocol);
|
const isHttpsRequest = isHttps.test(options.protocol);
|
||||||
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
|
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
|
||||||
if (config.transport) {
|
|
||||||
transport = config.transport;
|
if (isHttp2) {
|
||||||
} else if (config.maxRedirects === 0) {
|
transport = http2Transport;
|
||||||
transport = isHttpsRequest ? https : http;
|
|
||||||
} else {
|
} else {
|
||||||
if (config.maxRedirects) {
|
if (config.transport) {
|
||||||
options.maxRedirects = config.maxRedirects;
|
transport = config.transport;
|
||||||
|
} else if (config.maxRedirects === 0) {
|
||||||
|
transport = isHttpsRequest ? https : http;
|
||||||
|
} else {
|
||||||
|
if (config.maxRedirects) {
|
||||||
|
options.maxRedirects = config.maxRedirects;
|
||||||
|
}
|
||||||
|
if (config.beforeRedirect) {
|
||||||
|
options.beforeRedirects.config = config.beforeRedirect;
|
||||||
|
}
|
||||||
|
transport = isHttpsRequest ? httpsFollow : httpFollow;
|
||||||
}
|
}
|
||||||
if (config.beforeRedirect) {
|
|
||||||
options.beforeRedirects.config = config.beforeRedirect;
|
|
||||||
}
|
|
||||||
transport = isHttpsRequest ? httpsFollow : httpFollow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.maxBodyLength > -1) {
|
if (config.maxBodyLength > -1) {
|
||||||
@@ -484,7 +626,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
|
|
||||||
const streams = [res];
|
const streams = [res];
|
||||||
|
|
||||||
const responseLength = +res.headers['content-length'];
|
const responseLength = utils.toFiniteNumber(res.headers['content-length']);
|
||||||
|
|
||||||
if (onDownloadProgress || maxDownloadRate) {
|
if (onDownloadProgress || maxDownloadRate) {
|
||||||
const transformStream = new AxiosTransformStream({
|
const transformStream = new AxiosTransformStream({
|
||||||
@@ -547,10 +689,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
|
|
||||||
responseStream = streams.length > 1 ? stream.pipeline(streams, utils.noop) : streams[0];
|
responseStream = streams.length > 1 ? stream.pipeline(streams, utils.noop) : streams[0];
|
||||||
|
|
||||||
const offListeners = stream.finished(responseStream, () => {
|
|
||||||
offListeners();
|
|
||||||
onFinished();
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
status: res.statusCode,
|
status: res.statusCode,
|
||||||
@@ -562,7 +701,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
|
|
||||||
if (responseType === 'stream') {
|
if (responseType === 'stream') {
|
||||||
response.data = responseStream;
|
response.data = responseStream;
|
||||||
settle(resolve, reject, response);
|
settle(resolve, abort, response);
|
||||||
} else {
|
} else {
|
||||||
const responseBuffer = [];
|
const responseBuffer = [];
|
||||||
let totalResponseBytes = 0;
|
let totalResponseBytes = 0;
|
||||||
@@ -576,7 +715,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
// stream.destroy() emit aborted event before calling reject() on Node.js v16
|
// stream.destroy() emit aborted event before calling reject() on Node.js v16
|
||||||
rejected = true;
|
rejected = true;
|
||||||
responseStream.destroy();
|
responseStream.destroy();
|
||||||
reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded',
|
abort(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded',
|
||||||
AxiosError.ERR_BAD_RESPONSE, config, lastRequest));
|
AxiosError.ERR_BAD_RESPONSE, config, lastRequest));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -618,7 +757,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.once('abort', err => {
|
abortEmitter.once('abort', err => {
|
||||||
if (!responseStream.destroyed) {
|
if (!responseStream.destroyed) {
|
||||||
responseStream.emit('error', err);
|
responseStream.emit('error', err);
|
||||||
responseStream.destroy();
|
responseStream.destroy();
|
||||||
@@ -626,9 +765,12 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.once('abort', err => {
|
abortEmitter.once('abort', err => {
|
||||||
reject(err);
|
if (req.close) {
|
||||||
req.destroy(err);
|
req.close();
|
||||||
|
} else {
|
||||||
|
req.destroy(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
@@ -650,7 +792,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
const timeout = parseInt(config.timeout, 10);
|
const timeout = parseInt(config.timeout, 10);
|
||||||
|
|
||||||
if (Number.isNaN(timeout)) {
|
if (Number.isNaN(timeout)) {
|
||||||
reject(new AxiosError(
|
abort(new AxiosError(
|
||||||
'error trying to parse `config.timeout` to int',
|
'error trying to parse `config.timeout` to int',
|
||||||
AxiosError.ERR_BAD_OPTION_VALUE,
|
AxiosError.ERR_BAD_OPTION_VALUE,
|
||||||
config,
|
config,
|
||||||
@@ -672,13 +814,12 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
if (config.timeoutErrorMessage) {
|
if (config.timeoutErrorMessage) {
|
||||||
timeoutErrorMessage = config.timeoutErrorMessage;
|
timeoutErrorMessage = config.timeoutErrorMessage;
|
||||||
}
|
}
|
||||||
reject(new AxiosError(
|
abort(new AxiosError(
|
||||||
timeoutErrorMessage,
|
timeoutErrorMessage,
|
||||||
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
|
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
|
||||||
config,
|
config,
|
||||||
req
|
req
|
||||||
));
|
));
|
||||||
abort();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,7 +846,8 @@ export default isHttpAdapterSupported && function httpAdapter(config) {
|
|||||||
|
|
||||||
data.pipe(req);
|
data.pipe(req);
|
||||||
} else {
|
} else {
|
||||||
req.end(data);
|
data && req.write(data);
|
||||||
|
req.end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+39
@@ -66,6 +66,7 @@
|
|||||||
"rollup-plugin-auto-external": "^2.0.0",
|
"rollup-plugin-auto-external": "^2.0.0",
|
||||||
"rollup-plugin-bundle-size": "^1.0.3",
|
"rollup-plugin-bundle-size": "^1.0.3",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"selfsigned": "^3.0.1",
|
||||||
"sinon": "^4.5.0",
|
"sinon": "^4.5.0",
|
||||||
"stream-throttle": "^0.1.3",
|
"stream-throttle": "^0.1.3",
|
||||||
"string-replace-async": "^3.0.2",
|
"string-replace-async": "^3.0.2",
|
||||||
@@ -17909,6 +17910,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-forge": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-gyp": {
|
"node_modules/node-gyp": {
|
||||||
"version": "11.4.2",
|
"version": "11.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.4.2.tgz",
|
||||||
@@ -22316,6 +22327,19 @@
|
|||||||
"seek-table": "bin/seek-bzip-table"
|
"seek-table": "bin/seek-bzip-table"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/selfsigned": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-6U6w6kSLrM9Zxo0D7mC7QdGS6ZZytMWBnj/vhF9p+dAHx6CwGezuRcO4VclTbrrI7mg7SD6zNiqXUuBHOVopNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-forge": "^1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.3.8",
|
"version": "7.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
@@ -40788,6 +40812,12 @@
|
|||||||
"whatwg-url": "^5.0.0"
|
"whatwg-url": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node-forge": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node-gyp": {
|
"node-gyp": {
|
||||||
"version": "11.4.2",
|
"version": "11.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.4.2.tgz",
|
||||||
@@ -44115,6 +44145,15 @@
|
|||||||
"commander": "^2.8.1"
|
"commander": "^2.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"selfsigned": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-6U6w6kSLrM9Zxo0D7mC7QdGS6ZZytMWBnj/vhF9p+dAHx6CwGezuRcO4VclTbrrI7mg7SD6zNiqXUuBHOVopNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"node-forge": "^1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "7.3.8",
|
"version": "7.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"rollup-plugin-auto-external": "^2.0.0",
|
"rollup-plugin-auto-external": "^2.0.0",
|
||||||
"rollup-plugin-bundle-size": "^1.0.3",
|
"rollup-plugin-bundle-size": "^1.0.3",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"selfsigned": "^3.0.1",
|
||||||
"sinon": "^4.5.0",
|
"sinon": "^4.5.0",
|
||||||
"stream-throttle": "^0.1.3",
|
"stream-throttle": "^0.1.3",
|
||||||
"string-replace-async": "^3.0.2",
|
"string-replace-async": "^3.0.2",
|
||||||
|
|||||||
+45
-4
@@ -1,8 +1,10 @@
|
|||||||
import http from "http";
|
import http from "http";
|
||||||
|
import http2 from "http2";
|
||||||
import stream from "stream";
|
import stream from "stream";
|
||||||
import getStream from "get-stream";
|
import getStream from "get-stream";
|
||||||
import {Throttle} from "stream-throttle";
|
import {Throttle} from "stream-throttle";
|
||||||
import formidable from "formidable";
|
import formidable from "formidable";
|
||||||
|
import selfsigned from 'selfsigned';
|
||||||
|
|
||||||
|
|
||||||
export const LOCAL_SERVER_URL = 'http://localhost:4444';
|
export const LOCAL_SERVER_URL = 'http://localhost:4444';
|
||||||
@@ -11,15 +13,26 @@ export const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res);
|
|||||||
|
|
||||||
export const setTimeoutAsync = (ms) => new Promise(resolve=> setTimeout(resolve, ms));
|
export const setTimeoutAsync = (ms) => new Promise(resolve=> setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const certificate = selfsigned.generate(null, { keySize: 2048 });
|
||||||
|
|
||||||
export const startHTTPServer = (handlerOrOptions, options) => {
|
export const startHTTPServer = (handlerOrOptions, options) => {
|
||||||
|
|
||||||
const {handler, useBuffering = false, rate = undefined, port = 4444, keepAlive = 1000} =
|
const {
|
||||||
|
handler,
|
||||||
|
useBuffering = false,
|
||||||
|
rate = undefined,
|
||||||
|
port = 4444,
|
||||||
|
keepAlive = 1000,
|
||||||
|
useHTTP2,
|
||||||
|
key = certificate.private,
|
||||||
|
cert = certificate.cert,
|
||||||
|
} =
|
||||||
Object.assign(typeof handlerOrOptions === 'function' ? {
|
Object.assign(typeof handlerOrOptions === 'function' ? {
|
||||||
handler: handlerOrOptions
|
handler: handlerOrOptions
|
||||||
} : handlerOrOptions || {}, options);
|
} : handlerOrOptions || {}, options);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = http.createServer(handler || async function (req, res) {
|
const serverHandler = handler || async function (req, res) {
|
||||||
try {
|
try {
|
||||||
req.headers['content-length'] && res.setHeader('content-length', req.headers['content-length']);
|
req.headers['content-length'] && res.setHeader('content-length', req.headers['content-length']);
|
||||||
|
|
||||||
@@ -43,12 +56,36 @@ export const startHTTPServer = (handlerOrOptions, options) => {
|
|||||||
} catch (err){
|
} catch (err){
|
||||||
console.warn('HTTP server error:', err);
|
console.warn('HTTP server error:', err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}).listen(port, function (err) {
|
const server = useHTTP2 ?
|
||||||
|
http2.createSecureServer({key, cert} , serverHandler) :
|
||||||
|
http.createServer(serverHandler);
|
||||||
|
|
||||||
|
const sessions = new Set();
|
||||||
|
|
||||||
|
if(useHTTP2) {
|
||||||
|
server.on('session', (session) => {
|
||||||
|
sessions.add(session);
|
||||||
|
|
||||||
|
session.once('close', () => {
|
||||||
|
sessions.delete(session);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.closeAllSessions = () => {
|
||||||
|
for (const session of sessions) {
|
||||||
|
session.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
server.keepAliveTimeout = keepAlive;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.listen(port, function (err) {
|
||||||
err ? reject(err) : resolve(this);
|
err ? reject(err) : resolve(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.keepAliveTimeout = keepAlive;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +95,10 @@ export const stopHTTPServer = async (server, timeout = 10000) => {
|
|||||||
server.closeAllConnections();
|
server.closeAllConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof server.closeAllSessions === 'function') {
|
||||||
|
server.closeAllSessions();
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.race([new Promise(resolve => server.close(resolve)), setTimeoutAsync(timeout)]);
|
await Promise.race([new Promise(resolve => server.close(resolve)), setTimeoutAsync(timeout)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+424
-112
@@ -10,7 +10,7 @@ import assert from 'assert';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {lookup} from 'dns';
|
import {lookup} from 'dns';
|
||||||
let server, proxy;
|
let server, server2, proxy;
|
||||||
import AxiosError from '../../../lib/core/AxiosError.js';
|
import AxiosError from '../../../lib/core/AxiosError.js';
|
||||||
import FormDataLegacy from 'form-data';
|
import FormDataLegacy from 'form-data';
|
||||||
import formidable from 'formidable';
|
import formidable from 'formidable';
|
||||||
@@ -18,11 +18,23 @@ import express from 'express';
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
const isBlobSupported = typeof Blob !== 'undefined';
|
const isBlobSupported = typeof Blob !== 'undefined';
|
||||||
import {Throttle} from 'stream-throttle';
|
|
||||||
import devNull from 'dev-null';
|
import devNull from 'dev-null';
|
||||||
import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
|
import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
|
||||||
import {__setProxy} from "../../../lib/adapters/http.js";
|
import {__setProxy} from "../../../lib/adapters/http.js";
|
||||||
import {FormData as FormDataPolyfill, Blob as BlobPolyfill, File as FilePolyfill} from 'formdata-node';
|
import {FormData as FormDataPolyfill, Blob as BlobPolyfill, File as FilePolyfill} from 'formdata-node';
|
||||||
|
import getStream from "get-stream";
|
||||||
|
import {
|
||||||
|
startHTTPServer,
|
||||||
|
stopHTTPServer,
|
||||||
|
LOCAL_SERVER_URL,
|
||||||
|
SERVER_HANDLER_STREAM_ECHO,
|
||||||
|
handleFormData,
|
||||||
|
generateReadable
|
||||||
|
} from '../../helpers/server.js';
|
||||||
|
|
||||||
|
const LOCAL_SERVER_URL2 = 'https://localhost:5555';
|
||||||
|
const SERVER_PORT = 4444;
|
||||||
|
const SERVER_PORT2 = 5555;
|
||||||
|
|
||||||
const FormDataSpecCompliant = typeof FormData !== 'undefined' ? FormData : FormDataPolyfill;
|
const FormDataSpecCompliant = typeof FormData !== 'undefined' ? FormData : FormDataPolyfill;
|
||||||
const BlobSpecCompliant = typeof Blob !== 'undefined' ? Blob : BlobPolyfill;
|
const BlobSpecCompliant = typeof Blob !== 'undefined' ? Blob : BlobPolyfill;
|
||||||
@@ -31,8 +43,6 @@ const FileSpecCompliant = typeof File !== 'undefined' ? File : FilePolyfill;
|
|||||||
const __filename = url.fileURLToPath(import.meta.url);
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
import getStream from 'get-stream';
|
|
||||||
|
|
||||||
function setTimeoutAsync(ms) {
|
function setTimeoutAsync(ms) {
|
||||||
return new Promise(resolve=> setTimeout(resolve, ms));
|
return new Promise(resolve=> setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@@ -45,11 +55,11 @@ const deflateRaw = util.promisify(zlib.deflateRaw);
|
|||||||
const brotliCompress = util.promisify(zlib.brotliCompress);
|
const brotliCompress = util.promisify(zlib.brotliCompress);
|
||||||
|
|
||||||
function toleranceRange(positive, negative) {
|
function toleranceRange(positive, negative) {
|
||||||
const p = (1 + 1 / positive);
|
const p = 1 + positive / 100;
|
||||||
const n = (1 / negative);
|
const n = 1 - negative / 100;
|
||||||
|
|
||||||
return (actualValue, value) => {
|
return (actualValue, value) => {
|
||||||
return actualValue - value > 0 ? actualValue < value * p : actualValue > value * n;
|
return actualValue > value ? actualValue <= value * p : actualValue >= value * n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,100 +68,12 @@ const nodeMajorVersion = nodeVersion[0];
|
|||||||
|
|
||||||
var noop = ()=> {};
|
var noop = ()=> {};
|
||||||
|
|
||||||
const LOCAL_SERVER_URL = 'http://localhost:4444';
|
|
||||||
|
|
||||||
const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res);
|
|
||||||
|
|
||||||
function startHTTPServer(handlerOrOptions, options) {
|
|
||||||
|
|
||||||
const {handler, useBuffering = false, rate = undefined, port = 4444, keepAlive = 1000} =
|
|
||||||
Object.assign(typeof handlerOrOptions === 'function' ? {
|
|
||||||
handler: handlerOrOptions
|
|
||||||
} : handlerOrOptions || {}, options);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const server = http.createServer(handler || async function (req, res) {
|
|
||||||
try {
|
|
||||||
req.headers['content-length'] && res.setHeader('content-length', req.headers['content-length']);
|
|
||||||
|
|
||||||
var dataStream = req;
|
|
||||||
|
|
||||||
if (useBuffering) {
|
|
||||||
dataStream = stream.Readable.from(await getStream(req));
|
|
||||||
}
|
|
||||||
|
|
||||||
var streams = [dataStream];
|
|
||||||
|
|
||||||
if (rate) {
|
|
||||||
streams.push(new Throttle({rate}))
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.push(res);
|
|
||||||
|
|
||||||
stream.pipeline(streams, (err) => {
|
|
||||||
err && console.log('Server warning: ' + err.message)
|
|
||||||
});
|
|
||||||
} catch (err){
|
|
||||||
console.warn('HTTP server error:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
}).listen(port, function (err) {
|
|
||||||
err ? reject(err) : resolve(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.keepAliveTimeout = keepAlive;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopHTTPServer = async (server, timeout = 10000) => {
|
|
||||||
if (server) {
|
|
||||||
if (typeof server.closeAllConnections === 'function') {
|
|
||||||
server.closeAllConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([new Promise(resolve => server.close(resolve)), setTimeoutAsync(timeout)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFormData = (req) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const form = new formidable.IncomingForm();
|
|
||||||
|
|
||||||
form.parse(req, (err, fields, files) => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({fields, files});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateReadableStream(length = 1024 * 1024, chunkSize = 10 * 1024, sleep = 50) {
|
|
||||||
return stream.Readable.from(async function* (){
|
|
||||||
let dataLength = 0;
|
|
||||||
|
|
||||||
while(dataLength < length) {
|
|
||||||
const leftBytes = length - dataLength;
|
|
||||||
|
|
||||||
const chunk = Buffer.alloc(leftBytes > chunkSize? chunkSize : leftBytes);
|
|
||||||
|
|
||||||
dataLength += chunk.length;
|
|
||||||
|
|
||||||
yield chunk;
|
|
||||||
|
|
||||||
if (sleep) {
|
|
||||||
await setTimeoutAsync(sleep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('supports http with nodejs', function () {
|
describe('supports http with nodejs', function () {
|
||||||
afterEach(async function () {
|
afterEach(async function () {
|
||||||
await Promise.all([stopHTTPServer(server), stopHTTPServer(proxy)]);
|
await Promise.all([stopHTTPServer(server), stopHTTPServer(server2), stopHTTPServer(proxy)]);
|
||||||
|
|
||||||
server = null;
|
server = null;
|
||||||
|
server2 = null;
|
||||||
proxy = null;
|
proxy = null;
|
||||||
|
|
||||||
delete process.env.http_proxy;
|
delete process.env.http_proxy;
|
||||||
@@ -958,7 +880,7 @@ describe('supports http with nodejs', function () {
|
|||||||
it('should destroy the response stream with an error on request stream destroying', async function () {
|
it('should destroy the response stream with an error on request stream destroying', async function () {
|
||||||
server = await startHTTPServer();
|
server = await startHTTPServer();
|
||||||
|
|
||||||
let stream = generateReadableStream();
|
let stream = generateReadable();
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
stream.destroy();
|
stream.destroy();
|
||||||
@@ -2075,6 +1997,8 @@ describe('supports http with nodejs', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Rate limit', function () {
|
describe('Rate limit', function () {
|
||||||
|
this.timeout(30000);
|
||||||
|
|
||||||
it('should support upload rate limit', async function () {
|
it('should support upload rate limit', async function () {
|
||||||
const secs = 10;
|
const secs = 10;
|
||||||
const configRate = 100_000;
|
const configRate = 100_000;
|
||||||
@@ -2084,8 +2008,8 @@ describe('supports http with nodejs', function () {
|
|||||||
|
|
||||||
const buf = Buffer.alloc(chunkLength).fill('s');
|
const buf = Buffer.alloc(chunkLength).fill('s');
|
||||||
const samples = [];
|
const samples = [];
|
||||||
const skip = 2;
|
const skip = 4;
|
||||||
const compareValues = toleranceRange(10, 50);
|
const compareValues = toleranceRange(50, 50);
|
||||||
|
|
||||||
const {data} = await axios.post(LOCAL_SERVER_URL, buf, {
|
const {data} = await axios.post(LOCAL_SERVER_URL, buf, {
|
||||||
onUploadProgress: ({loaded, total, progress, bytes, rate}) => {
|
onUploadProgress: ({loaded, total, progress, bytes, rate}) => {
|
||||||
@@ -2132,8 +2056,8 @@ describe('supports http with nodejs', function () {
|
|||||||
|
|
||||||
const buf = Buffer.alloc(chunkLength).fill('s');
|
const buf = Buffer.alloc(chunkLength).fill('s');
|
||||||
const samples = [];
|
const samples = [];
|
||||||
const skip = 2;
|
const skip = 4;
|
||||||
const compareValues = toleranceRange(10, 50);
|
const compareValues = toleranceRange(50, 50);
|
||||||
|
|
||||||
const {data} = await axios.post(LOCAL_SERVER_URL, buf, {
|
const {data} = await axios.post(LOCAL_SERVER_URL, buf, {
|
||||||
onDownloadProgress: ({loaded, total, progress, bytes, rate}) => {
|
onDownloadProgress: ({loaded, total, progress, bytes, rate}) => {
|
||||||
@@ -2173,7 +2097,9 @@ describe('supports http with nodejs', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('request aborting', function() {
|
describe('request aborting', function() {
|
||||||
it('should be able to abort the response stream', async function () {
|
//this.timeout(5000);
|
||||||
|
|
||||||
|
it('should be able to abort the response stream', async () => {
|
||||||
server = await startHTTPServer({
|
server = await startHTTPServer({
|
||||||
rate: 100_000,
|
rate: 100_000,
|
||||||
useBuffering: true
|
useBuffering: true
|
||||||
@@ -2183,7 +2109,7 @@ describe('supports http with nodejs', function () {
|
|||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
var {data} = await axios.post(LOCAL_SERVER_URL, buf, {
|
const {data} = await axios.post(LOCAL_SERVER_URL, buf, {
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
maxRedirects: 0
|
maxRedirects: 0
|
||||||
@@ -2199,14 +2125,9 @@ describe('supports http with nodejs', function () {
|
|||||||
streamError = err;
|
streamError = err;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
await assert.rejects(() => pipelineAsync([data, devNull()]));
|
||||||
await pipelineAsync(data, devNull());
|
|
||||||
assert.fail('stream was not aborted');
|
assert.strictEqual(streamError && streamError.code, 'ERR_CANCELED');
|
||||||
} catch(e) {
|
|
||||||
console.log(`pipeline error: ${e}`);
|
|
||||||
} finally {
|
|
||||||
assert.strictEqual(streamError && streamError.code, 'ERR_CANCELED');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2350,4 +2271,395 @@ describe('supports http with nodejs', function () {
|
|||||||
assert.deepStrictEqual(data, {foo: 'success'});
|
assert.deepStrictEqual(data, {foo: 'success'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('HTTP2', function () {
|
||||||
|
const LOCAL_SERVER_URL = 'https://127.0.0.1:4444';
|
||||||
|
|
||||||
|
const http2Axios = axios.create({
|
||||||
|
baseURL: LOCAL_SERVER_URL,
|
||||||
|
httpVersion: 2,
|
||||||
|
http2Options: {
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge request http2Options with its instance config', async () => {
|
||||||
|
const {data} = await http2Axios.get('/', {
|
||||||
|
http2Options: {
|
||||||
|
foo : 'test'
|
||||||
|
},
|
||||||
|
adapter: async (config) => {
|
||||||
|
return {
|
||||||
|
data: config.http2Options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(data, {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
foo : 'test'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support http2 transport', async () => {
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
res.end('OK');
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const {data} = await http2Axios.get(LOCAL_SERVER_URL);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(data, 'OK');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should support request payload`, async () => {
|
||||||
|
server = await startHTTPServer(null, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = 'DATA';
|
||||||
|
|
||||||
|
const {data} = await http2Axios.post(LOCAL_SERVER_URL, payload);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(data, payload);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should support FormData as a payload`, async function () {
|
||||||
|
if (typeof FormData !== 'function') {
|
||||||
|
this.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
server = await startHTTPServer(async (req, res) => {
|
||||||
|
const {fields, files} = await handleFormData(req);
|
||||||
|
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
fields,
|
||||||
|
files
|
||||||
|
}));
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
|
||||||
|
form.append('x', 'foo');
|
||||||
|
form.append('y', 'bar');
|
||||||
|
|
||||||
|
const {data} = await http2Axios.post(LOCAL_SERVER_URL, form);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(data, {
|
||||||
|
fields: {
|
||||||
|
x: 'foo',
|
||||||
|
y: 'bar'
|
||||||
|
},
|
||||||
|
files: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("response types", () => {
|
||||||
|
const originalData = '{"test": "OK"}';
|
||||||
|
|
||||||
|
const fixtures = {
|
||||||
|
'text' : (v) => assert.strictEqual(v, originalData),
|
||||||
|
'arraybuffer' : (v) => assert.deepStrictEqual(v, Buffer.from(originalData)),
|
||||||
|
'stream': async (v) => assert.deepStrictEqual(await getStream(v), originalData),
|
||||||
|
'json': async (v) => assert.deepStrictEqual(v, JSON.parse(originalData))
|
||||||
|
};
|
||||||
|
|
||||||
|
for(let [responseType, assertValue] of Object.entries(fixtures)) {
|
||||||
|
it(`should support ${responseType} response type`, async () => {
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
res.end(originalData);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const {data} = await http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertValue(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
it('should support request timeout', async () => {
|
||||||
|
let isAborted= false;
|
||||||
|
|
||||||
|
let aborted;
|
||||||
|
const promise = new Promise(resolve => aborted = resolve);
|
||||||
|
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res.end('OK');
|
||||||
|
}, 15000);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('stream', (stream) => {
|
||||||
|
stream.once('aborted', () => {
|
||||||
|
isAborted = true;
|
||||||
|
aborted();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(async () => {
|
||||||
|
await http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
timeout: 500
|
||||||
|
});
|
||||||
|
}, /timeout/);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
assert.ok(isAborted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support request cancellation', async function (){
|
||||||
|
if (typeof AbortSignal !== 'function') {
|
||||||
|
this.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAborted= false;
|
||||||
|
|
||||||
|
let aborted;
|
||||||
|
const promise = new Promise(resolve => aborted = resolve);
|
||||||
|
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res.end('OK');
|
||||||
|
}, 15000);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('stream', (stream) => {
|
||||||
|
stream.once('aborted', () => {
|
||||||
|
isAborted = true;
|
||||||
|
aborted();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(async () => {
|
||||||
|
await http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
signal: AbortSignal.timeout(500)
|
||||||
|
});
|
||||||
|
}, /CanceledError: canceled/);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
assert.ok(isAborted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support stream response cancellation', async () => {
|
||||||
|
let isAborted= false;
|
||||||
|
var source = axios.CancelToken.source();
|
||||||
|
|
||||||
|
let aborted;
|
||||||
|
const promise = new Promise(resolve => aborted = resolve);
|
||||||
|
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
generateReadable(10000, 100, 100).pipe(res);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('stream', (stream) => {
|
||||||
|
stream.once('aborted', () => {
|
||||||
|
isAborted = true;
|
||||||
|
aborted();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const {data} = await http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
cancelToken: source.token,
|
||||||
|
responseType: 'stream'
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => source.cancel());
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => pipelineAsync([data, devNull()]),
|
||||||
|
/CanceledError: canceled/
|
||||||
|
)
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
assert.ok(isAborted);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("session", () => {
|
||||||
|
it("should reuse session for the target authority", async() => {
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => res.end('OK'), 1000);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const [response1, response2] = await Promise.all([
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream'
|
||||||
|
}),
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream'
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(response1.data.session, response2.data.session);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
await Promise.all([
|
||||||
|
getStream(response1.data),
|
||||||
|
getStream(response2.data)
|
||||||
|
]),
|
||||||
|
['OK', 'OK']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use different sessions for different authorities", async() => {
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => res.end('OK'), 1000);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
server2 = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => res.end('OK'), 1000);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true,
|
||||||
|
port: SERVER_PORT2
|
||||||
|
});
|
||||||
|
|
||||||
|
const [response1, response2] = await Promise.all([
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream'
|
||||||
|
}),
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL2, {
|
||||||
|
responseType: 'stream'
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.notStrictEqual(response1.data.session, response2.data.session);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
await Promise.all([
|
||||||
|
getStream(response1.data),
|
||||||
|
getStream(response2.data)
|
||||||
|
]),
|
||||||
|
['OK', 'OK']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use different sessions for requests with different http2Options set", async() => {
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => res.end('OK'), 1000);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const [response1, response2] = await Promise.all([
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream',
|
||||||
|
http2Options: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream',
|
||||||
|
http2Options: {
|
||||||
|
foo: 'test'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.notStrictEqual(response1.data.session, response2.data.session);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
await Promise.all([
|
||||||
|
getStream(response1.data),
|
||||||
|
getStream(response2.data)
|
||||||
|
]),
|
||||||
|
['OK', 'OK']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use the same session for request with the same resolved http2Options set", async() => {
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => res.end('OK'), 1000);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const responses = await Promise.all([
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream'
|
||||||
|
}),
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream',
|
||||||
|
http2Options: undefined
|
||||||
|
}),
|
||||||
|
http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream',
|
||||||
|
http2Options: {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
assert.strictEqual(responses[1].data.session, responses[0].data.session);
|
||||||
|
assert.strictEqual(responses[2].data.session, responses[0].data.session);
|
||||||
|
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
await Promise.all(responses.map(({data}) => getStream(data))),
|
||||||
|
['OK', 'OK', 'OK']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use different sessions after previous session timeout", async() => {
|
||||||
|
server = await startHTTPServer((req, res) => {
|
||||||
|
setTimeout(() => res.end('OK'), 100);
|
||||||
|
}, {
|
||||||
|
useHTTP2: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const response1 = await http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream',
|
||||||
|
http2Options: {
|
||||||
|
sessionTimeout: 1000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await setTimeoutAsync(5000);
|
||||||
|
|
||||||
|
const response2 = await http2Axios.get(LOCAL_SERVER_URL, {
|
||||||
|
responseType: 'stream',
|
||||||
|
http2Options: {
|
||||||
|
sessionTimeout: 1000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.notStrictEqual(response1.data.session, response2.data.session);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
await Promise.all([
|
||||||
|
getStream(response1.data),
|
||||||
|
getStream(response2.data)
|
||||||
|
]),
|
||||||
|
['OK', 'OK']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user