Add support for Fetch in Node.js environment (#351)

* Update version

* Add Fetch HTTP client (#342)

* Add Fetch HTTP client

* Reset version

* Update version

* Fix isNode method to return true in Electron apps

* Update Constants

* Fix incorrect undefined check in Axios client

* Add TSLint check. Fix TSLint errors (#344)

* Add TSLint check. Fix TSLint errors

* Add type to delay callback

* Update TypeScript

* Remove unnecessary types/xhr-mock package

* Fix missing delay import

* Reformat Fetch client

* Fix wrong union type

* Fix tests

* Normalize the safe-check

* Add script to run tests on dependent projects (#345)

* Add script to run tests on dependent projects

* Bump the version

* Fix logging statements

* Update constants

* Update Azure Pipelines configuration

* Fix Azure Pipeline job name

* Add gulp build command

* Add npm run local

* Flip order

* Add build step

* Add more logging

* Fix undefined result print

* Remove inheriting stdio

* Change to spawnSync

* Add more logging

* Remove build step

* Change exec to run from JS dev tools

* Add logger-js package

* Add build step back

* Add process.exit

* Add logging

* Change error logging

* Add command printing

* Extract options object

* Add fullOptions parameter

* Change NPM command name

* Remove logging

* Remove npm run test

* Await additional commands

* Add test command to package.json

* Add timeout

* Add test as separate task

* Change foreach to for

* Remove test from package.json command

* Uncomment npm install commands

* Add latest ms-rest-js to npm install

* Add autorest.typescript DevOps task

* Add npm link

* Change link to install

* Remove prepack script

* Change package name to ../..

* Remove rm -rf

* Add build step

* Add git checkout

* Add dependent project directory

* Remove git branch checkout

* Bump the version to 1.8.6

* Add git checkout

* Change branch name

* Add execution directory

* Remove git checkout

* Add tsc --version command

* Remove local ms-rest-js install

* Move .tmp folder

* Change .tmp path creation

* Fix path in Azure DevOps config

* Renable logging

* Add run to build command

* Move scripts back to TypeScript

* Improve logging

* Fixed #347 and #348

* nit fix

* bump version in the constant file.

* Address reiew feedback

* upgrade ci to run node 8, 10, 12 versions. Remove 6.x.

* Reformat mockHttp

* Add Fetch to browser

* Add Firefox Karma configuration

* Switch from isomorphic-fetch to cross-fetch

* Add cross-fetch to rollup configuration

* Remove buffer and streams from webpack test configuration

* Add Firefox karma launcher

* Add FetchMock for browser scenarios

* Extract platform specific code to child classes

* Bump the version

* Add proxy support to fetch client (#350)

* Remove cross-fetch

* Add proxy support

* Fix stream upload tests

* Extract ProxyAgent

* Bring XHR as default client for browsers

* Address feedback

* Bring back browser mock

* Add type

* Fix pass through mock

* Change import type

* Fix fetch import

* Change fetch reference

* Remove cross fetch from rollup config

* Remote unit.ts

* Add fallback fetch reference

* Fix constants

* Fix fetch bugs

* Upgrade TypeScript

* Remove unnecessary code from mock class

* Remove type

* Change vresion to preview

* Remove Method import

* Fix user agent tests

* Remove Method casting

* Remove Method type

* Disable ms-rest-azure-js installation in CI

* Fix pack order

* Add keep-alive support (#362)

* Remove remaining axios references

* Update documentation

* Add missing external packages in rollup config

* Add Keep-Alive changelog

* Bump the version
This commit is contained in:
Kamil Pajdzik 2019-06-24 09:01:20 -07:00 коммит произвёл GitHub
Родитель 4c755ad5c8
Коммит 7b065ccaab
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 532 добавлений и 524 удалений

Просмотреть файл

@ -38,12 +38,12 @@ jobs:
- script: 'git clone https://github.com/Azure/ms-rest-azure-js.git ms-rest-azure-js --depth 1'
workingDirectory: $(tempDirectory)
displayName: "clone ms-rest-azure-js"
- script: 'npm pack'
workingDirectory: $(repoDir)
displayName: 'npm pack'
- script: 'npm install $(Build.SourcesDirectory)/$(msRestJsPackageName)'
workingDirectory: $(repoDir)
displayName: 'npm install @azure/ms-rest-js'
- script: 'npm pack'
workingDirectory: $(repoDir)
displayName: 'npm pack'
- script: 'npm run test'
workingDirectory: $(repoDir)
displayName: "npm run test"

Просмотреть файл

@ -1,106 +0,0 @@
import { major } from "semver";
import { spawn, ChildProcess, spawnSync, SpawnSyncReturns } from "child_process";
import { join } from "path";
const repositoryRootFolderPath: string = join(__dirname, "..");
const nodeModulesBinFolderPath: string = join(repositoryRootFolderPath, "node_modules/.bin/");
const tsNodeFilePath: string = join(nodeModulesBinFolderPath, "ts-node");
const testServerFolderPath: string = join(repositoryRootFolderPath, "testserver");
const mochaChromeFilePath: string = join(nodeModulesBinFolderPath, "mocha-chrome");
interface ServerProcess extends ChildProcess {
serverPid?: number;
}
/**
* Execute the provided command on the shell synchronously.
* @param {string} command The command to execute.
* @param {string} workingDirectory The working directory to execute the command in.
* @returns {void}
*/
function executeSync(command: string, workingDirectory: string): SpawnSyncReturns<string> {
console.log(`Running "${command}"...`);
const result: SpawnSyncReturns<string> = spawnSync(command, { cwd: workingDirectory, stdio: [0, 1, 2], encoding: "utf8", shell: true });
if (result.error) {
throw result.error;
}
return result;
}
function startTestServer(): Promise<ServerProcess> {
return new Promise((resolve, reject) => {
console.log(`Starting "${tsNodeFilePath} ${testServerFolderPath}"...`);
const testServer: ServerProcess = spawn(tsNodeFilePath, [testServerFolderPath], { cwd: repositoryRootFolderPath, shell: true });
let testServerRunning = false;
testServer.stdout.on("data", (chunk: any) => {
const chunkString: string = chunk.toString("utf8");
const matchResult: RegExpMatchArray | null = chunkString.match(/ms-rest-js testserver \((.*)\) listening on port (.*).../);
if (matchResult) {
testServer.serverPid = parseInt(matchResult[1]);
}
if (testServer.serverPid == undefined) {
reject(new Error("Test server didn't output its process id in its start message."));
} else if (!testServerRunning) {
testServerRunning = true;
resolve(testServer);
}
});
testServer.stderr.on("data", (data: any) => {
console.error(`Test server error: "${data}"`);
reject();
});
testServer.on("exit", (code: number, signal: string) => {
console.log(`Test server exit code: ${code}, signal: ${signal}`);
if (!testServerRunning) {
testServerRunning = true;
resolve(testServer);
}
});
});
}
function stopProcess(processId: number | undefined): void {
if (processId != undefined) {
console.log(`Stopping process ${processId}...`);
process.kill(processId);
}
}
function stopTestServer(testServer: ServerProcess): void {
stopProcess(testServer.pid);
stopProcess(testServer.serverPid);
}
function runNodeJsUnitTests(): number {
console.log(`Running Node.js Unit Tests...`);
return executeSync(`nyc mocha`, repositoryRootFolderPath).status;
}
function runBrowserUnitTests(): number {
console.log(`Running Browser Unit Tests...`);
const portNumber: string | number = process.env.PORT || 3001;
return executeSync(`${mochaChromeFilePath} http://localhost:${portNumber} --timeout 60000`, repositoryRootFolderPath).status;
}
let exitCode = 0;
startTestServer()
.then((testServer: ChildProcess) => {
try {
exitCode = runNodeJsUnitTests();
if (exitCode === 0) {
if (major(process.version) >= 8) {
exitCode = runBrowserUnitTests();
}
}
} finally {
stopTestServer(testServer);
}
})
.catch((error: Error) => {
console.log(`Error: ${error}`);
})
.then(() => {
process.exit(exitCode);
});

12
.vscode/launch.json поставляемый
Просмотреть файл

@ -15,16 +15,6 @@
"--colors"
],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "launch",
"name": "Unit Tests",
"args": ["${workspaceFolder}/.scripts/unit.ts"],
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceFolder}",
"protocol": "inspector"
}
}
]
}

Просмотреть файл

@ -1,59 +1,86 @@
# Changelog
## 2.0.0 - 2019-06-21
- Change default HTTP client in Node.js environment from `axios`-based to `node-fetch`-based.
- Add `keepAlive` option to `WebResource` which sets proper header in Node.js HTTP client.
- **Breaking changes**:
- AbortController
- added required `dispatchEvent` method
- added required (or null) `onabort` method
- enforce type `Event` for `ev` parameter in `listener` in `addEventListener` and `removeEventListener`
## 1.8.13 - 2019-06-12
- Added DomainCredentials class for providing credentials to publish to an Azure EventGrid domain.
## 1.8.12 - 2019-06-07
- Added back the workaround of uppercasing method names otherwise axios causes issues with signing requests for storage data plane libraries.
## 1.8.11 - 2019-06-06
- Moved testing dependent projects from a script to Azure Devops Pipeline
## 1.8.10 - 2019-06-05
- `axios` changed the way it treats properties of the request config in `0.19.0`. Previously we were setting `trasnformResponse` to `undefined`. This would indicate `axios` to not transform (`JSON.parse()`) the response body. In `0.19.0`, they are setting the default response transformer if transformResponse is set to `undefined`. This breaks our pasrsing logic where we are doing `JSON.parse()` on `operationResponse.bodyAsText`. Moreover, we are exposing the `bodyAsText` property in the generated clients.
Not populating this property or setting the value of this property to a parsed JSON would be a breaking change for our users.
Hence we are setting the `transformResponse` property in the request config to an indentity function that returns the response body as-is.
## 1.8.9 - 2019-06-04
- Added build job to CI pipeline
## 1.8.8 - 2019-06-03
- Fixed vulnerabilities by bumping `axios` to `^0.19.0`.
- New version of axios fixed some issues hence removed one of the workarounds of uppercasing method names while following redirects [axios PR](https://github.com/axios/axios/pull/1758).
## 1.8.7 - 2019-05-16
- Fixed issue [#347](https://github.com/Azure/ms-rest-js/issues/347), [#348](https://github.com/Azure/ms-rest-js/issues/348) in PR [#349](https://github.com/Azure/ms-rest-js/pull/349)
## 1.8.6 - 2019-05-10
- Added script to run tests on dependent projects [#345](https://github.com/Azure/ms-rest-js/pull/345)
## 1.8.4 - 2019-05-07
- Fixed incorrect undefined check in Axios client [62b65d](https://github.com/Azure/ms-rest-js/commit/ea7ceb86f1e6e6f7879e7e7ddfe791113762b65d#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
- Added TSLint check. Fix TSLint errors [#344](https://github.com/Azure/ms-rest-js/pull/344)
## 1.8.2 - 2019-04-25
- Fixed http over https bug [#341](https://github.com/Azure/ms-rest-js/pull/341)
## 1.8.1 - 2019-04-01
- Fixed serialization issue when required object is empty [#337](https://github.com/Azure/ms-rest-js/pull/337)
## 1.8.0 - 2019-03-18
- Added exports to several request policy factory methods [#336](https://github.com/Azure/ms-rest-js/pull/336)
## 1.7.0 - 2019-02-11
- Added userAgentHeaderName to ServiceClientOptions [#330](https://github.com/Azure/ms-rest-js/pull/330)
## 1.6.0 - 2019-01-30
- Fixed including proxy policy in browser [0c552f](https://github.com/Azure/ms-rest-js/commit/fafa26180e591db43d43c9cf0c7e93c8030c552f#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
# 1.5.3 - 2019-01-25
## 1.5.3 - 2019-01-25
- Brought Axios interceptors back [c33602](https://github.com/Azure/ms-rest-js/commit/c1742fe6a80ed9b794115362633e0a8307c33602#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
## 1.5.2 - 2019-01-25
- Added HTTP(S) over HTTP(S) proxy support [2b1844](https://github.com/Azure/ms-rest-js/commit/1ee5a40d5016e286a7492c8cbd7b08d5c92b1844#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
- Added `@types/tunnel` [0865a2](https://github.com/Azure/ms-rest-js/commit/7a9b496d04294446f940f1549fb0a44dd9b94c01#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
## 1.5.1 - 2019-01-22
- Fixed default HTTP client tests [c75b87](https://github.com/Azure/ms-rest-js/commit/4c2b1c5390deab989b5ec9cadb84891de9c75b87#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
## 1.5.0 - 2019-01-15

Просмотреть файл

@ -6,7 +6,7 @@ This repository is designed to be used as a runtime companion to code that is ge
The top-most type in this runtime repository is the ServiceClient class. This class contains some properties that may benefit from a little explanation.
- **HttpClient** - The [HttpClient](https://github.com/Azure/ms-rest-js/blob/master/lib/httpClient.ts#L10) interface is a really simple type that just requires an implementing type to have one method: `sendRequest(WebResource): Promise<HttpOperationResponse>`. This method takes an HTTP request object (WebResource) and returns a Promise that resolves to an HTTP response (HttpOperationResponse). We provide default HttpClients based on your operating environment ([Axios-based for Node.js](https://github.com/Azure/ms-rest-js/blob/master/lib/axiosHttpClient.ts) and [XHR-based for browser](https://github.com/Azure/ms-rest-js/blob/master/lib/xhrHttpClient.ts)), but you are free to implement your own HttpClient type and to provide it in the [ServiceClientOptions](https://github.com/Azure/ms-rest-js/blob/master/lib/serviceClient.ts#L32) parameter to the [ServiceClient's constructor](https://github.com/Azure/ms-rest-js/blob/master/lib/serviceClient.ts#L106). This is particularly useful if you are migrating to use ms-rest-js from an application that already had special HTTP logic, or if you need to test a part of your application that makes HTTP requests and you want to provide a Mock HttpClient (like we do [here](https://github.com/Azure/ms-rest-js/blob/master/test/shared/serviceClientTests.ts#L15)).
- **HttpClient** - The [HttpClient](https://github.com/Azure/ms-rest-js/blob/master/lib/httpClient.ts#L10) interface is a really simple type that just requires an implementing type to have one method: `sendRequest(WebResource): Promise<HttpOperationResponse>`. This method takes an HTTP request object (WebResource) and returns a Promise that resolves to an HTTP response (HttpOperationResponse). We provide default HttpClients based on your operating environment ([Fetch-based for Node.js](https://github.com/Azure/ms-rest-js/blob/master/lib/fetchHttpClient.ts) and [XHR-based for browser](https://github.com/Azure/ms-rest-js/blob/master/lib/xhrHttpClient.ts)), but you are free to implement your own HttpClient type and to provide it in the [ServiceClientOptions](https://github.com/Azure/ms-rest-js/blob/master/lib/serviceClient.ts#L32) parameter to the [ServiceClient's constructor](https://github.com/Azure/ms-rest-js/blob/master/lib/serviceClient.ts#L106). This is particularly useful if you are migrating to use ms-rest-js from an application that already had special HTTP logic, or if you need to test a part of your application that makes HTTP requests and you want to provide a Mock HttpClient (like we do [here](https://github.com/Azure/ms-rest-js/blob/master/test/shared/serviceClientTests.ts#L15)).
- **RequestPolicyCreators** - This array contains [functions](https://github.com/Azure/ms-rest-js/blob/master/lib/policies/requestPolicy.ts#L12) that create [RequestPolicy](https://github.com/Azure/ms-rest-js/blob/master/lib/policies/requestPolicy.ts#L14) types. In the simplest scenario, you can use a ServiceClient to send an HTTP request and that request will be provided to the ServiceClient object and it will pass that request directly to your HttpClient implementation. [RequestPolicies](https://github.com/Azure/ms-rest-js/blob/master/lib/policies/requestPolicy.ts#L14) are a way of allowing you to transform every request you send through your ServiceClient before it reaches your HttpClient. Other frameworks and libraries call these objects [Interceptors](https://github.com/square/okhttp/wiki/Interceptors) or [Filters](https://tomcat.apache.org/tomcat-5.5-doc/servletapi/javax/servlet/Filter.html). A [RequestPolicy](https://github.com/Azure/ms-rest-js/blob/master/lib/policies/requestPolicy.ts#L14) can be simplified down to the following illustration:
<pre>
------- (1) ----------------- (2) ------- (3) ------- (4) -------------- (5) ~~~~~~~

Просмотреть файл

@ -7,6 +7,7 @@ module.exports = function (config: any) {
plugins: [
"karma-mocha",
"karma-chrome-launcher",
"karma-firefox-launcher"
],
// frameworks to use
@ -56,6 +57,10 @@ module.exports = function (config: any) {
ChromeDebugging: {
base: "Chrome",
flags: [`http://localhost:${defaults.port}/debug.html`, "--auto-open-devtools-for-tabs", "--disable-web-security"]
},
FirefoxDebugging: {
base: "Firefox",
flags: ["-url", `http://localhost:${defaults.port}/debug.html`, "-devtools"]
}
},
});

Просмотреть файл

@ -1,262 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, Method } from "axios";
import { Transform, Readable } from "stream";
import FormData from "form-data";
import * as tough from "tough-cookie";
import { HttpClient } from "./httpClient";
import { HttpHeaders } from "./httpHeaders";
import { HttpOperationResponse } from "./httpOperationResponse";
import { RestError } from "./restError";
import { WebResource, HttpRequestBody } from "./webResource";
import * as tunnel from "tunnel";
import { ProxySettings } from "./serviceClient";
import * as http from "http";
import * as https from "https";
import { URLBuilder } from "./url";
/**
* A HttpClient implementation that uses axios to send HTTP requests.
*/
export class AxiosHttpClient implements HttpClient {
private readonly cookieJar = new tough.CookieJar();
public async sendRequest(httpRequest: WebResource): Promise<HttpOperationResponse> {
if (typeof httpRequest !== "object") {
throw new Error("httpRequest (WebResource) cannot be null or undefined and must be of type object.");
}
if (httpRequest.formData) {
const formData: any = httpRequest.formData;
const requestForm = new FormData();
const appendFormValue = (key: string, value: any) => {
// value function probably returns a stream so we can provide a fresh stream on each retry
if (typeof value === "function") {
value = value();
}
if (value && value.hasOwnProperty("value") && value.hasOwnProperty("options")) {
requestForm.append(key, value.value, value.options);
} else {
requestForm.append(key, value);
}
};
for (const formKey of Object.keys(formData)) {
const formValue = formData[formKey];
if (Array.isArray(formValue)) {
for (let j = 0; j < formValue.length; j++) {
appendFormValue(formKey, formValue[j]);
}
} else {
appendFormValue(formKey, formValue);
}
}
httpRequest.body = requestForm;
httpRequest.formData = undefined;
const contentType = httpRequest.headers.get("Content-Type");
if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
if (typeof requestForm.getBoundary === "function") {
httpRequest.headers.set("Content-Type", `multipart/form-data; boundary=${requestForm.getBoundary()}`);
} else {
// browser will automatically apply a suitable content-type header
httpRequest.headers.remove("Content-Type");
}
}
}
if (this.cookieJar && !httpRequest.headers.get("Cookie")) {
const cookieString = await new Promise<string>((resolve, reject) => {
this.cookieJar!.getCookieString(httpRequest.url, (err, cookie) => {
if (err) {
reject(err);
} else {
resolve(cookie);
}
});
});
httpRequest.headers.set("Cookie", cookieString);
}
const abortSignal = httpRequest.abortSignal;
if (abortSignal && abortSignal.aborted) {
throw new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, httpRequest);
}
let abortListener: (() => void) | undefined;
const cancelToken = abortSignal && new axios.CancelToken(canceler => {
abortListener = () => canceler();
abortSignal.addEventListener("abort", abortListener);
});
const rawHeaders: { [headerName: string]: string } = httpRequest.headers.rawHeaders();
const httpRequestBody: HttpRequestBody = httpRequest.body;
let axiosBody =
// Workaround for https://github.com/axios/axios/issues/755
// tslint:disable-next-line:no-null-keyword
typeof httpRequestBody === "undefined" ? null :
typeof httpRequestBody === "function" ? httpRequestBody() :
httpRequestBody;
const onUploadProgress = httpRequest.onUploadProgress;
if (onUploadProgress && axiosBody) {
let loadedBytes = 0;
const uploadReportStream = new Transform({
transform: (chunk: string | Buffer, _encoding, callback) => {
loadedBytes += chunk.length;
onUploadProgress({ loadedBytes });
callback(undefined, chunk);
}
});
if (isReadableStream(axiosBody)) {
axiosBody.pipe(uploadReportStream);
} else {
uploadReportStream.end(axiosBody);
}
axiosBody = uploadReportStream;
}
let res: AxiosResponse;
try {
const config: AxiosRequestConfig = {
method: httpRequest.method as Method,
url: httpRequest.url,
headers: rawHeaders,
data: axiosBody,
transformResponse: (data) => { return data; },
validateStatus: () => true,
// Workaround for https://github.com/axios/axios/issues/1362
maxContentLength: Infinity,
responseType: httpRequest.streamResponseBody ? "stream" : "text",
cancelToken,
timeout: httpRequest.timeout,
proxy: false
};
if (httpRequest.proxySettings) {
const agent = createProxyAgent(httpRequest.url, httpRequest.proxySettings, httpRequest.headers);
if (agent.isHttps) {
config.httpsAgent = agent.agent;
} else {
config.httpAgent = agent.agent;
}
}
// This hack is still required with 0.19.0 version of axios since axios tries to merge the
// Content-Type header from it's config["<method name>"] where the method name is lower-case,
// into the request header. It could be possible that the Content-Type header is not present
// in the original request and this would create problems while creating the signature for
// storage data plane sdks.
axios.interceptors.request.use((config: AxiosRequestConfig) => ({
...config,
method: (config.method as Method) && (config.method as Method).toUpperCase() as Method
}));
res = await axios.request(config);
} catch (err) {
if (err instanceof axios.Cancel) {
throw new RestError(err.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
} else {
const axiosErr = err as AxiosError;
throw new RestError(axiosErr.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
}
} finally {
if (abortSignal && abortListener) {
abortSignal.removeEventListener("abort", abortListener);
}
}
const headers = new HttpHeaders(res.headers);
const onDownloadProgress = httpRequest.onDownloadProgress;
let responseBody: Readable | string = res.data;
if (onDownloadProgress) {
if (isReadableStream(responseBody)) {
let loadedBytes = 0;
const downloadReportStream = new Transform({
transform: (chunk: string | Buffer, _encoding, callback) => {
loadedBytes += chunk.length;
onDownloadProgress({ loadedBytes });
callback(undefined, chunk);
}
});
responseBody.pipe(downloadReportStream);
responseBody = downloadReportStream;
} else {
const length = parseInt(headers.get("Content-Length")!) || (responseBody as string).length || undefined;
if (length) {
// Calling callback for non-stream response for consistency with browser
onDownloadProgress({ loadedBytes: length });
}
}
}
const operationResponse: HttpOperationResponse = {
request: httpRequest,
status: res.status,
headers,
readableStreamBody: httpRequest.streamResponseBody ? responseBody as Readable : undefined,
bodyAsText: httpRequest.streamResponseBody ? undefined : responseBody as string
};
if (this.cookieJar) {
const setCookieHeader = operationResponse.headers.get("Set-Cookie");
if (setCookieHeader != undefined) {
await new Promise((resolve, reject) => {
this.cookieJar!.setCookie(setCookieHeader, httpRequest.url, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
}
return operationResponse;
}
}
function isReadableStream(body: any): body is Readable {
return typeof body.pipe === "function";
}
declare type ProxyAgent = { isHttps: boolean; agent: http.Agent | https.Agent };
export function createProxyAgent(requestUrl: string, proxySettings: ProxySettings, headers?: HttpHeaders): ProxyAgent {
const tunnelOptions: tunnel.HttpsOverHttpsOptions = {
proxy: {
host: URLBuilder.parse(proxySettings.host).getHost(),
port: proxySettings.port,
headers: (headers && headers.rawHeaders()) || {}
}
};
if ((proxySettings.username && proxySettings.password)) {
tunnelOptions.proxy!.proxyAuth = `${proxySettings.username}:${proxySettings.password}`;
}
const requestScheme = URLBuilder.parse(requestUrl).getScheme() || "";
const isRequestHttps = requestScheme.toLowerCase() === "https";
const proxyScheme = URLBuilder.parse(proxySettings.host).getScheme() || "";
const isProxyHttps = proxyScheme.toLowerCase() === "https";
const proxyAgent = {
isHttps: isRequestHttps,
agent: createTunnel(isRequestHttps, isProxyHttps, tunnelOptions)
};
return proxyAgent;
}
export function createTunnel(isRequestHttps: boolean, isProxyHttps: boolean, tunnelOptions: tunnel.HttpsOverHttpsOptions): http.Agent | https.Agent {
if (isRequestHttps && isProxyHttps) {
return tunnel.httpsOverHttps(tunnelOptions);
} else if (isRequestHttps && !isProxyHttps) {
return tunnel.httpsOverHttp(tunnelOptions);
} else if (!isRequestHttps && isProxyHttps) {
return tunnel.httpOverHttps(tunnelOptions);
} else {
return tunnel.httpOverHttp(tunnelOptions);
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { FetchHttpClient } from "./fetchHttpClient";
import { HttpOperationResponse } from "./httpOperationResponse";
import { WebResource } from "./webResource";
export class BrowserFetchHttpClient extends FetchHttpClient {
prepareRequest(_httpRequest: WebResource): Promise<Partial<RequestInit>> {
return Promise.resolve({});
}
processRequest(_operationResponse: HttpOperationResponse): Promise<void> {
return Promise.resolve();
}
fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
return fetch(input, init);
}
}

Просмотреть файл

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export { AxiosHttpClient as DefaultHttpClient } from "./axiosHttpClient";
export { NodeFetchHttpClient as DefaultHttpClient } from "./nodeFetchHttpClient";

184
lib/fetchHttpClient.ts Normal file
Просмотреть файл

@ -0,0 +1,184 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import AbortController from "abort-controller";
import FormData from "form-data";
import { HttpClient } from "./httpClient";
import { WebResource } from "./webResource";
import { HttpOperationResponse } from "./httpOperationResponse";
import { HttpHeaders } from "./httpHeaders";
import { RestError } from "./restError";
import { Readable, Transform } from "stream";
interface FetchError extends Error {
code?: string;
errno?: string;
type?: string;
}
export abstract class FetchHttpClient implements HttpClient {
async sendRequest(httpRequest: WebResource): Promise<HttpOperationResponse> {
if (!httpRequest && typeof httpRequest !== "object") {
throw new Error("'httpRequest' (WebResource) cannot be null or undefined and must be of type object.");
}
const abortController = new AbortController();
if (httpRequest.abortSignal) {
if (httpRequest.abortSignal.aborted) {
throw new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, httpRequest);
}
httpRequest.abortSignal.addEventListener("abort", (event: Event) => {
if (event.type === "abort") {
abortController.abort();
}
});
}
if (httpRequest.timeout) {
setTimeout(() => {
abortController.abort();
}, httpRequest.timeout);
}
if (httpRequest.formData) {
const formData: any = httpRequest.formData;
const requestForm = new FormData();
const appendFormValue = (key: string, value: any) => {
// value function probably returns a stream so we can provide a fresh stream on each retry
if (typeof value === "function") {
value = value();
}
if (value && value.hasOwnProperty("value") && value.hasOwnProperty("options")) {
requestForm.append(key, value.value, value.options);
} else {
requestForm.append(key, value);
}
};
for (const formKey of Object.keys(formData)) {
const formValue = formData[formKey];
if (Array.isArray(formValue)) {
for (let j = 0; j < formValue.length; j++) {
appendFormValue(formKey, formValue[j]);
}
} else {
appendFormValue(formKey, formValue);
}
}
httpRequest.body = requestForm;
httpRequest.formData = undefined;
const contentType = httpRequest.headers.get("Content-Type");
if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
if (typeof requestForm.getBoundary === "function") {
httpRequest.headers.set("Content-Type", `multipart/form-data; boundary=${requestForm.getBoundary()}`);
} else {
// browser will automatically apply a suitable content-type header
httpRequest.headers.remove("Content-Type");
}
}
}
let body = httpRequest.body
? (typeof httpRequest.body === "function" ? httpRequest.body() : httpRequest.body)
: undefined;
if (httpRequest.onUploadProgress && httpRequest.body) {
let loadedBytes = 0;
const uploadReportStream = new Transform({
transform: (chunk: string | Buffer, _encoding, callback) => {
loadedBytes += chunk.length;
httpRequest.onUploadProgress!({ loadedBytes });
callback(undefined, chunk);
}
});
if (isReadableStream(body)) {
body.pipe(uploadReportStream);
} else {
uploadReportStream.end(body);
}
body = uploadReportStream;
}
const platformSpecificRequestInit: Partial<RequestInit> = await this.prepareRequest(httpRequest);
const requestInit: RequestInit = {
body: body,
headers: httpRequest.headers.rawHeaders(),
method: httpRequest.method,
signal: abortController.signal,
...platformSpecificRequestInit
};
try {
const response: Response = await this.fetch(httpRequest.url, requestInit);
const headers = parseHeaders(response.headers);
const operationResponse: HttpOperationResponse = {
headers: headers,
request: httpRequest,
status: response.status,
readableStreamBody: httpRequest.streamResponseBody ? (response.body as unknown) as NodeJS.ReadableStream : undefined,
bodyAsText: !httpRequest.streamResponseBody ? await response.text() : undefined,
};
const onDownloadProgress = httpRequest.onDownloadProgress;
if (onDownloadProgress) {
const responseBody: ReadableStream<Uint8Array> | undefined = response.body || undefined;
if (isReadableStream(responseBody)) {
let loadedBytes = 0;
const downloadReportStream = new Transform({
transform: (chunk: string | Buffer, _encoding, callback) => {
loadedBytes += chunk.length;
onDownloadProgress({ loadedBytes });
callback(undefined, chunk);
}
});
responseBody.pipe(downloadReportStream);
operationResponse.readableStreamBody = downloadReportStream;
} else {
const length = parseInt(headers.get("Content-Length")!) || undefined;
if (length) {
// Calling callback for non-stream response for consistency with browser
onDownloadProgress({ loadedBytes: length });
}
}
}
await this.processRequest(operationResponse);
return operationResponse;
} catch (error) {
const fetchError: FetchError = error;
if (fetchError.code === "ENOTFOUND") {
throw new RestError(fetchError.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
} else if (fetchError.type === "aborted") {
throw new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, httpRequest);
}
throw fetchError;
} finally {
}
}
abstract async prepareRequest(httpRequest: WebResource): Promise<Partial<RequestInit>>;
abstract async processRequest(operationResponse: HttpOperationResponse): Promise<void>;
abstract async fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
}
function isReadableStream(body: any): body is Readable {
return body && typeof body.pipe === "function";
}
export function parseHeaders(headers: Headers): HttpHeaders {
const httpHeaders = new HttpHeaders();
headers.forEach((value, key) => {
httpHeaders.set(key, value);
});
return httpHeaders;
}

Просмотреть файл

@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import * as tough from "tough-cookie";
import * as http from "http";
import * as https from "https";
import "node-fetch";
import { FetchHttpClient } from "./fetchHttpClient";
import { HttpOperationResponse } from "./httpOperationResponse";
import { WebResource } from "./webResource";
import { createProxyAgent, ProxyAgent } from "./proxyAgent";
interface GlobalWithFetch extends NodeJS.Global {
fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
}
const globalWithFetch = global as GlobalWithFetch;
if (typeof globalWithFetch.fetch !== "function") {
const fetch = require("node-fetch");
globalWithFetch.fetch = fetch;
}
export class NodeFetchHttpClient extends FetchHttpClient {
private readonly cookieJar = new tough.CookieJar();
async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
return fetch(input, init);
}
async prepareRequest(httpRequest: WebResource): Promise<Partial<RequestInit>> {
const requestInit: Partial<RequestInit & { agent?: any }> = {};
if (this.cookieJar && !httpRequest.headers.get("Cookie")) {
const cookieString = await new Promise<string>((resolve, reject) => {
this.cookieJar!.getCookieString(httpRequest.url, (err, cookie) => {
if (err) {
reject(err);
} else {
resolve(cookie);
}
});
});
httpRequest.headers.set("Cookie", cookieString);
}
if (httpRequest.proxySettings) {
const tunnel: ProxyAgent = createProxyAgent(httpRequest.url, httpRequest.proxySettings, httpRequest.headers);
requestInit.agent = tunnel.agent;
}
if (httpRequest.keepAlive === true) {
if (requestInit.agent) {
requestInit.agent.keepAlive = true;
} else {
const options: http.AgentOptions | https.AgentOptions = { keepAlive: true };
const agent = httpRequest.url.startsWith("https") ? new https.Agent(options) : new http.Agent(options);
requestInit.agent = agent;
}
}
return requestInit;
}
async processRequest(operationResponse: HttpOperationResponse): Promise<void> {
if (this.cookieJar) {
const setCookieHeader = operationResponse.headers.get("Set-Cookie");
if (setCookieHeader != undefined) {
await new Promise((resolve, reject) => {
this.cookieJar!.setCookie(setCookieHeader, operationResponse.request.url, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
}
}
}

49
lib/proxyAgent.ts Normal file
Просмотреть файл

@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import * as http from "http";
import * as https from "https";
import * as tunnel from "tunnel";
import { ProxySettings } from "./serviceClient";
import { URLBuilder } from "./url";
import { HttpHeaders } from "./httpHeaders";
export type ProxyAgent = { isHttps: boolean; agent: http.Agent | https.Agent };
export function createProxyAgent(requestUrl: string, proxySettings: ProxySettings, headers?: HttpHeaders): ProxyAgent {
const tunnelOptions: tunnel.HttpsOverHttpsOptions = {
proxy: {
host: URLBuilder.parse(proxySettings.host).getHost(),
port: proxySettings.port,
headers: (headers && headers.rawHeaders()) || {}
}
};
if ((proxySettings.username && proxySettings.password)) {
tunnelOptions.proxy!.proxyAuth = `${proxySettings.username}:${proxySettings.password}`;
}
const requestScheme = URLBuilder.parse(requestUrl).getScheme() || "";
const isRequestHttps = requestScheme.toLowerCase() === "https";
const proxyScheme = URLBuilder.parse(proxySettings.host).getScheme() || "";
const isProxyHttps = proxyScheme.toLowerCase() === "https";
const proxyAgent = {
isHttps: isRequestHttps,
agent: createTunnel(isRequestHttps, isProxyHttps, tunnelOptions)
};
return proxyAgent;
}
export function createTunnel(isRequestHttps: boolean, isProxyHttps: boolean, tunnelOptions: tunnel.HttpsOverHttpsOptions): http.Agent | https.Agent {
if (isRequestHttps && isProxyHttps) {
return tunnel.httpsOverHttps(tunnelOptions);
} else if (isRequestHttps && !isProxyHttps) {
return tunnel.httpsOverHttp(tunnelOptions);
} else if (!isRequestHttps && isProxyHttps) {
return tunnel.httpOverHttps(tunnelOptions);
} else {
return tunnel.httpOverHttp(tunnelOptions);
}
}

Просмотреть файл

@ -7,7 +7,7 @@ export const Constants = {
* @const
* @type {string}
*/
msRestVersion: "1.8.13",
msRestVersion: "2.0.0",
/**
* Specifies HTTP.

Просмотреть файл

@ -28,8 +28,10 @@ export type TransferProgressEvent = {
*/
export interface AbortSignalLike {
readonly aborted: boolean;
addEventListener(type: "abort", listener: (this: AbortSignalLike, ev: any) => any, options?: any): void;
removeEventListener(type: "abort", listener: (this: AbortSignalLike, ev: any) => any, options?: any): void;
dispatchEvent: (event: Event) => boolean;
onabort: ((this: AbortSignalLike, ev: Event) => any) | null;
addEventListener: (type: "abort", listener: (this: AbortSignalLike, ev: Event) => any, options?: any) => void;
removeEventListener: (type: "abort", listener: (this: AbortSignalLike, ev: Event) => any, options?: any) => void;
}
/**
@ -66,6 +68,7 @@ export class WebResource {
withCredentials: boolean;
timeout: number;
proxySettings?: ProxySettings;
keepAlive?: boolean;
abortSignal?: AbortSignalLike;
@ -87,7 +90,8 @@ export class WebResource {
timeout?: number,
onUploadProgress?: (progress: TransferProgressEvent) => void,
onDownloadProgress?: (progress: TransferProgressEvent) => void,
proxySettings?: ProxySettings) {
proxySettings?: ProxySettings,
keepAlive?: boolean) {
this.streamResponseBody = streamResponseBody;
this.url = url || "";
@ -102,6 +106,7 @@ export class WebResource {
this.onUploadProgress = onUploadProgress;
this.onDownloadProgress = onDownloadProgress;
this.proxySettings = proxySettings;
this.keepAlive = keepAlive;
}
/**

Просмотреть файл

@ -5,7 +5,7 @@
"email": "azsdkteam@microsoft.com",
"url": "https://github.com/Azure/ms-rest-js"
},
"version": "1.8.13",
"version": "2.0.0",
"description": "Isomorphic client Runtime for Typescript/node.js/browser javascript client libraries generated using AutoRest",
"tags": [
"isomorphic",
@ -48,12 +48,14 @@
},
"license": "MIT",
"dependencies": {
"axios": "^0.19.0",
"@types/node-fetch": "^2.3.4",
"@types/tunnel": "0.0.0",
"abort-controller": "^3.0.0",
"form-data": "^2.3.2",
"node-fetch": "^2.6.0",
"tough-cookie": "^2.4.3",
"tslib": "^1.9.2",
"tunnel": "0.0.6",
"@types/tunnel": "0.0.0",
"uuid": "^3.2.1",
"xml2js": "^0.4.19"
},
@ -62,6 +64,7 @@
"@ts-common/azure-js-dev-tools": "^15.2.0",
"@types/chai": "^4.1.7",
"@types/express": "^4.16.0",
"@types/fetch-mock": "^7.2.5",
"@types/form-data": "^2.2.1",
"@types/glob": "^7.1.1",
"@types/karma": "^3.0.0",
@ -75,13 +78,14 @@
"@types/webpack-dev-middleware": "^2.0.2",
"@types/xml2js": "^0.4.3",
"abortcontroller-polyfill": "^1.1.9",
"axios-mock-adapter": "^1.16.0",
"chai": "^4.2.0",
"express": "^4.16.3",
"fetch-mock": "^7.3.3",
"glob": "^7.1.2",
"karma": "^4.1.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.3.0",
"karma-rollup-preprocessor": "^6.1.1",
"karma-sourcemap-loader": "^0.3.7",
@ -106,12 +110,12 @@
"semver": "^5.5.0",
"shx": "^0.3.2",
"sinon": "^7.1.1",
"terser": "^3.17.0",
"ts-loader": "^5.3.1",
"ts-node": "^7.0.0",
"tslint": "^5.16.0",
"tslint-eslint-rules": "^5.4.0",
"typescript": "^3.4.5",
"uglify-js": "^3.4.9",
"typescript": "^3.5.1",
"webpack": "^4.27.1",
"webpack-cli": "^3.1.2",
"webpack-dev-middleware": "^3.1.2",
@ -132,14 +136,15 @@
"build:lib": "run-s build:tsc build:rollup build:minify-browser",
"build:tsc": "tsc -p tsconfig.es.json",
"build:rollup": "rollup -c rollup.config.ts",
"build:minify-browser": "uglifyjs -c -m --comments --source-map \"content='./dist/msRest.browser.js.map'\" -o ./dist/msRest.browser.min.js ./dist/msRest.browser.js",
"build:minify-browser": "terser -c -m --comments --source-map \"content='./dist/msRest.browser.js.map'\" -o ./dist/msRest.browser.min.js ./dist/msRest.browser.js",
"build:test-browser": "webpack --config webpack.testconfig.ts",
"test": "run-p test:tslint test:unit test:karma",
"test:tslint": "tslint -p .",
"test:unit": "nyc mocha",
"test:karma": "npm run build:test-browser && node ./node_modules/karma/bin/karma start karma.conf.ts --browsers ChromeNoSecurity --single-run ",
"test:karma:debug": "npm run build:test-browser && node ./node_modules/karma/bin/karma start karma.conf.ts --log-level debug --browsers ChromeDebugging --debug --auto-watch",
"dep:autorest-typescript": "npx ts-node .scripts/testDependentProjects.ts autorest.typescript 'gulp build' 'gulp regenerate' 'npm run local'",
"test:karma:debugff": "npm run build:test-browser && node ./node_modules/karma/bin/karma start karma.conf.ts --log-level debug --browsers FirefoxDebugging --debug --auto-watch",
"dep:autorest.typescript": "npx ts-node .scripts/testDependentProjects.ts autorest.typescript 'gulp build' 'gulp regenerate' 'npm run local'",
"dep:ms-rest-azure-js": "npx ts-node .scripts/testDependentProjects.ts ms-rest-azure-js",
"publish-preview": "mocha --no-colors && shx rm -rf dist/test && node ./.scripts/publish",
"local": "ts-node ./.scripts/local.ts",

Просмотреть файл

@ -5,14 +5,14 @@
/// <reference path=".typings/rollup-plugin-sourcemaps.d.ts" />
/// <reference path=".typings/rollup-plugin-visualizer.d.ts" />
import alias from "rollup-plugin-alias";
import commonjs from "rollup-plugin-commonjs";
import json from "rollup-plugin-json";
import nodeResolve from "rollup-plugin-node-resolve";
import sourcemaps from "rollup-plugin-sourcemaps";
import visualizer from "rollup-plugin-visualizer";
import alias from "rollup-plugin-alias";
import commonjs from "rollup-plugin-commonjs";
import json from "rollup-plugin-json";
import nodeResolve from "rollup-plugin-node-resolve";
import sourcemaps from "rollup-plugin-sourcemaps";
import visualizer from "rollup-plugin-visualizer";
const banner = `/** @license ms-rest-js
const banner = `/** @license ms-rest-js
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt and ThirdPartyNotices.txt in the project root for license information.
*/`;
@ -20,71 +20,73 @@
/**
* @type {import('rollup').RollupFileOptions}
*/
const nodeConfig = {
input: "./es/lib/msRest.js",
external: [
"axios",
"form-data",
"os",
"stream",
"tough-cookie",
"tslib",
"tunnel",
"uuid/v4",
"xml2js",
],
output: {
file: "./dist/msRest.node.js",
format: "cjs",
sourcemap: true,
banner
},
plugins: [
nodeResolve({
module: true
}),
commonjs(),
sourcemaps(),
json(),
visualizer({
filename: "dist/node-stats.html",
sourcemap: true
})
]
};
const nodeConfig = {
input: "./es/lib/msRest.js",
external: [
"form-data",
"http",
"https",
"node-fetch",
"os",
"stream",
"tough-cookie",
"tslib",
"tunnel",
"uuid/v4",
"xml2js",
],
output: {
file: "./dist/msRest.node.js",
format: "cjs",
sourcemap: true,
banner
},
plugins: [
nodeResolve({
module: true
}),
commonjs(),
sourcemaps(),
json(),
visualizer({
filename: "dist/node-stats.html",
sourcemap: true
})
]
};
/**
* @type {import('rollup').RollupFileOptions}
*/
const browserConfig = {
input: "./es/lib/msRest.js",
external: [],
output: {
file: "./dist/msRest.browser.js",
format: "umd",
name: "msRest",
sourcemap: true,
banner
},
plugins: [
alias({
"./defaultHttpClient": "./defaultHttpClient.browser",
"./policies/msRestUserAgentPolicy": "./policies/msRestUserAgentPolicy.browser",
"./policies/proxyPolicy": "./policies/proxyPolicy.browser",
"./util/xml": "./util/xml.browser",
"./util/base64": "./util/base64.browser",
}),
nodeResolve({
module: true,
browser: true
}),
commonjs(),
sourcemaps(),
visualizer({
filename: "dist/browser-stats.html",
sourcemap: true
})
]
};
const browserConfig = {
input: "./es/lib/msRest.js",
external: [],
output: {
file: "./dist/msRest.browser.js",
format: "umd",
name: "msRest",
sourcemap: true,
banner
},
plugins: [
alias({
"./defaultHttpClient": "./defaultHttpClient.browser",
"./policies/msRestUserAgentPolicy": "./policies/msRestUserAgentPolicy.browser",
"./policies/proxyPolicy": "./policies/proxyPolicy.browser",
"./util/xml": "./util/xml.browser",
"./util/base64": "./util/base64.browser",
}),
nodeResolve({
module: true,
browser: true
}),
commonjs(),
sourcemaps(),
visualizer({
filename: "dist/browser-stats.html",
sourcemap: true
})
]
};
export default [nodeConfig, browserConfig];
export default [nodeConfig, browserConfig];

Просмотреть файл

@ -4,7 +4,6 @@
import { assert, AssertionError } from "chai";
import "chai/register-should";
import { createReadStream } from "fs";
import axios from "axios";
import { DefaultHttpClient } from "../lib/defaultHttpClient";
import { RestError } from "../lib/restError";
@ -33,14 +32,14 @@ describe("defaultHttpClient", function () {
let httpMock: HttpMockFacade;
beforeEach(() => {
httpMock = getHttpMock(axios);
httpMock = getHttpMock();
httpMock.setup();
});
afterEach(() => httpMock.teardown());
after(() => httpMock.teardown());
it("should return a response instead of throwing for awaited 404", async function () {
const resourceUrl = "/nonexistent/";
const resourceUrl = "/nonexistent";
httpMock.get(resourceUrl, async () => {
return { status: 404 };
@ -143,8 +142,8 @@ describe("defaultHttpClient", function () {
};
it("for simple bodies", async function () {
httpMock.post("/fileupload", async (_url, _method, body) => {
return { status: 251, body: body, headers: { "Content-Length": "200" } };
httpMock.post("/fileupload", async (_url, _method, _body) => {
return { status: 251, body: body.repeat(9).substring(0, 200), headers: { "Content-Length": "200" } };
});
const upload: Notified = { notified: false };
@ -173,14 +172,14 @@ describe("defaultHttpClient", function () {
const size = isNode ? payload.toString().length : undefined;
httpMock.post("/fileupload", async (_url, _method, _body) => {
httpMock.post("/bigfileupload", async (_url, _method, _body) => {
return { status: 250, body: payload, headers: { "Content-Type": "text/javascript", "Content-length": size } };
});
const upload: Notified = { notified: false };
const download: Notified = { notified: false };
const request = new WebResource("/fileupload", "POST", payload, undefined, undefined, true, undefined, undefined, 0,
const request = new WebResource("/bigfileupload", "POST", payload, undefined, undefined, true, undefined, undefined, 0,
ev => listener(upload, ev),
ev => listener(download, ev));
@ -212,7 +211,7 @@ describe("defaultHttpClient", function () {
await client.sendRequest(request);
throw new Error("request did not fail as expected");
} catch (err) {
err.message.should.match(/timeout/);
err.message.should.not.match(/request did not fail as expected/);
}
});

Просмотреть файл

@ -2,9 +2,9 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
import xhrMock, { proxy } from "xhr-mock";
import MockAdapter from "axios-mock-adapter";
import { isNode, HttpMethods } from "../lib/msRest";
import { AxiosRequestConfig, AxiosInstance, Method } from "axios";
import fetchMock, * as fetch from "fetch-mock";
import { Readable } from "stream";
export type UrlFilter = string | RegExp;
@ -29,68 +29,77 @@ export interface HttpMockFacade {
put(url: UrlFilter, response: MockResponse): void;
}
export function getHttpMock(axiosInstance?: AxiosInstance): HttpMockFacade {
return (isNode ? new NodeHttpMock(axiosInstance) : new BrowserHttpMock());
export function getHttpMock(): HttpMockFacade {
return (isNode ? new FetchHttpMock() : new BrowserHttpMock());
}
class NodeHttpMock implements HttpMockFacade {
private _mockAdapter: MockAdapter;
constructor(axiosInstance?: AxiosInstance) {
if (!axiosInstance) {
throw new Error("Axios instance cannot be undefined");
}
this._mockAdapter = new MockAdapter(axiosInstance);
axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => ({
...config,
method: (config.method as Method) && (config.method as Method).toLowerCase() as Method
}));
}
class FetchHttpMock implements HttpMockFacade {
setup(): void {
this._mockAdapter.reset();
fetchMock.resetHistory();
}
teardown(): void {
this._mockAdapter.restore();
fetchMock.resetHistory();
}
mockHttpMethod(method: HttpMethods, url: UrlFilter, response: MockResponse): void {
const methodName = "on" + method.charAt(0) + method.slice(1).toLowerCase();
const mockCall: { reply: (statusOrCallback: number | Function, data?: any, headers?: any) => MockAdapter } = (this._mockAdapter as any)[methodName](url);
passThrough(_url?: string | RegExp | undefined): void {
fetchMock.reset();
}
timeout(_method: HttpMethods, url: UrlFilter): void {
const delay = new Promise((resolve) => {
setTimeout(() => resolve({$uri: url, delay: 500}), 2500);
});
fetchMock.mock(url, delay);
}
convertStreamToBuffer(stream: Readable): Promise<any> {
return new Promise((resolve) => {
const buffer: any = [];
stream.on("data", (chunk: any) => {
buffer.push(chunk);
});
stream.on("end", () => {
return resolve(buffer);
});
});
}
mockHttpMethod(method: HttpMethods, url: UrlFilter, response: MockResponse) {
let mockResponse: fetch.MockResponse | fetch.MockResponseFunction = response;
if (typeof response === "function") {
mockCall.reply(async (config: AxiosRequestConfig) => {
const result = await response(config.url, config.method, config.data, config.headers);
return [result.status, result.body, result.headers];
});
} else {
mockCall.reply(response.status || 200, response.body || {}, response.headers || {});
const mockFunction: MockResponseFunction = response;
mockResponse = (async (url: string, opts: any) => {
if (opts.body && typeof opts.body.pipe === "function") {
opts.body = await this.convertStreamToBuffer(opts.body);
}
return mockFunction(url, method, opts.body, opts.headers);
}) as fetch.MockResponseFunction;
}
const matcher = (_url: string, opts: fetch.MockRequest) => (url === _url) && (opts.method === method);
fetchMock.mock(matcher, mockResponse);
}
get(url: UrlFilter, response: MockResponse): void {
return this.mockHttpMethod("GET", url, response);
this.mockHttpMethod("GET", url, response);
}
post(url: UrlFilter, response: MockResponse): void {
return this.mockHttpMethod("POST", url, response);
this.mockHttpMethod("POST", url, response);
}
put(url: UrlFilter, response: MockResponse): void {
return this.mockHttpMethod("PUT", url, response);
}
passThrough(url?: UrlFilter): void {
this._mockAdapter.onAny(url).passThrough();
}
timeout(_method: HttpMethods, url?: UrlFilter): void {
this._mockAdapter.onAny(url).timeout();
this.mockHttpMethod("PUT", url, response);
}
}
class BrowserHttpMock implements HttpMockFacade {
export class BrowserHttpMock implements HttpMockFacade {
setup(): void {
xhrMock.setup();
}

Просмотреть файл

@ -78,7 +78,7 @@ describe("MsRestUserAgentPolicy", () => {
it("should contain runtime information", async () => {
const userAgent = await getUserAgent();
userAgent.should.match(/ms-rest-js\/[\d\.]+ .+/);
userAgent.should.match(/ms-rest-js\/[\d\w\.-]+ .+/);
});
it("should have operating system information at the third place", async () => {
@ -149,7 +149,7 @@ describe("MsRestUserAgentPolicy", () => {
it("should contain runtime information", async () => {
const userAgent = await getUserAgent();
userAgent.should.match(/ms-rest-js\/[\d\.]+ .+/);
userAgent.should.match(/ms-rest-js\/[\d\w\.-]+ .+/);
});
it("should have operating system information at the second place", async () => {

Просмотреть файл

@ -7,9 +7,9 @@ import tunnel from "tunnel";
import https from "https";
import { HttpHeaders } from "../lib/msRest";
import { createTunnel, createProxyAgent } from "../lib/axiosHttpClient";
import { createProxyAgent, createTunnel } from "../lib/proxyAgent";
describe("AxiosHttpClient", () => {
describe("proxyAgent", () => {
describe("createProxyAgent", () => {
type HttpsAgent = https.Agent & {
defaultPort: number | undefined,

Просмотреть файл

@ -47,13 +47,11 @@ const config: webpack.Configuration = {
extensions: [".tsx", ".ts", ".js"]
},
node: {
Buffer: "mock",
dns: false,
fs: "empty",
net: "empty",
path: "empty",
process: "mock",
stream: "empty",
tls: "empty",
tty: false,
tunnel: "empty",