зеркало из https://github.com/Azure/ms-rest-js.git
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:
Родитель
4c755ad5c8
Коммит
7b065ccaab
|
@ -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"
|
||||
|
|
106
.scripts/unit.ts
106
.scripts/unit.ts
|
@ -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);
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
29
Changelog.md
29
Changelog.md
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
21
package.json
21
package.json
|
@ -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",
|
||||
|
|
142
rollup.config.ts
142
rollup.config.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",
|
||||
|
|
Загрузка…
Ссылка в новой задаче