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);
});

14
.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) ~~~~~~~
@ -24,4 +24,4 @@ The top-most type in this runtime repository is the ServiceClient class. This cl
5. The HttpClient implementation now does whatever it needs to do to send the HTTP request across the network. Most likely the code that sends HTTP requests doesn't know how to handle a WebResource, so the HttpClient first needs to convert the WebResource HTTP request into the type that the real HTTP implementation knows how to deal with. Then it sends that converted request across the network.
6. Somehow the HttpClient will get an asynchronous response from the Network (either via callback or Promise). Either way, that response needs to be converted to a Promise<HttpOperationResponse> and returned to the previous RequestPolicy in the pipeline.
7. The RequestPolicies are free to either return the response as they receive it, or they can perform additional logic based on the response (such as [retrying a failed request](https://github.com/Azure/ms-rest-js/blob/master/lib/policies/exponentialRetryPolicy.ts#L42) or [deserializing the response's headers and/or body](https://github.com/Azure/ms-rest-js/blob/master/lib/policies/deserializationPolicy.ts#L28)).
8. When the HTTP response has finally been returned from the first RequestPolicy in the pipeline, the ServiceClient returns it to your application's code, where you can handle the response however you want.
8. When the HTTP response has finally been returned from the first RequestPolicy in the pipeline, the ServiceClient returns it to your application's code, where you can handle the response however you want.

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

@ -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",