зеркало из 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'
|
- script: 'git clone https://github.com/Azure/ms-rest-azure-js.git ms-rest-azure-js --depth 1'
|
||||||
workingDirectory: $(tempDirectory)
|
workingDirectory: $(tempDirectory)
|
||||||
displayName: "clone ms-rest-azure-js"
|
displayName: "clone ms-rest-azure-js"
|
||||||
- script: 'npm pack'
|
|
||||||
workingDirectory: $(repoDir)
|
|
||||||
displayName: 'npm pack'
|
|
||||||
- script: 'npm install $(Build.SourcesDirectory)/$(msRestJsPackageName)'
|
- script: 'npm install $(Build.SourcesDirectory)/$(msRestJsPackageName)'
|
||||||
workingDirectory: $(repoDir)
|
workingDirectory: $(repoDir)
|
||||||
displayName: 'npm install @azure/ms-rest-js'
|
displayName: 'npm install @azure/ms-rest-js'
|
||||||
|
- script: 'npm pack'
|
||||||
|
workingDirectory: $(repoDir)
|
||||||
|
displayName: 'npm pack'
|
||||||
- script: 'npm run test'
|
- script: 'npm run test'
|
||||||
workingDirectory: $(repoDir)
|
workingDirectory: $(repoDir)
|
||||||
displayName: "npm run test"
|
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"
|
"--colors"
|
||||||
],
|
],
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
"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
|
# 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
|
## 1.8.13 - 2019-06-12
|
||||||
|
|
||||||
- Added DomainCredentials class for providing credentials to publish to an Azure EventGrid domain.
|
- Added DomainCredentials class for providing credentials to publish to an Azure EventGrid domain.
|
||||||
|
|
||||||
## 1.8.12 - 2019-06-07
|
## 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.
|
- 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
|
## 1.8.11 - 2019-06-06
|
||||||
|
|
||||||
- Moved testing dependent projects from a script to Azure Devops Pipeline
|
- Moved testing dependent projects from a script to Azure Devops Pipeline
|
||||||
|
|
||||||
## 1.8.10 - 2019-06-05
|
## 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.
|
- `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.
|
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.
|
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
|
## 1.8.9 - 2019-06-04
|
||||||
|
|
||||||
- Added build job to CI pipeline
|
- Added build job to CI pipeline
|
||||||
|
|
||||||
## 1.8.8 - 2019-06-03
|
## 1.8.8 - 2019-06-03
|
||||||
|
|
||||||
- Fixed vulnerabilities by bumping `axios` to `^0.19.0`.
|
- 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).
|
- 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
|
## 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)
|
- 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
|
## 1.8.6 - 2019-05-10
|
||||||
|
|
||||||
- Added script to run tests on dependent projects [#345](https://github.com/Azure/ms-rest-js/pull/345)
|
- Added script to run tests on dependent projects [#345](https://github.com/Azure/ms-rest-js/pull/345)
|
||||||
|
|
||||||
## 1.8.4 - 2019-05-07
|
## 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)
|
- 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)
|
- Added TSLint check. Fix TSLint errors [#344](https://github.com/Azure/ms-rest-js/pull/344)
|
||||||
|
|
||||||
## 1.8.2 - 2019-04-25
|
## 1.8.2 - 2019-04-25
|
||||||
|
|
||||||
- Fixed http over https bug [#341](https://github.com/Azure/ms-rest-js/pull/341)
|
- Fixed http over https bug [#341](https://github.com/Azure/ms-rest-js/pull/341)
|
||||||
|
|
||||||
## 1.8.1 - 2019-04-01
|
## 1.8.1 - 2019-04-01
|
||||||
|
|
||||||
- Fixed serialization issue when required object is empty [#337](https://github.com/Azure/ms-rest-js/pull/337)
|
- Fixed serialization issue when required object is empty [#337](https://github.com/Azure/ms-rest-js/pull/337)
|
||||||
|
|
||||||
## 1.8.0 - 2019-03-18
|
## 1.8.0 - 2019-03-18
|
||||||
|
|
||||||
- Added exports to several request policy factory methods [#336](https://github.com/Azure/ms-rest-js/pull/336)
|
- Added exports to several request policy factory methods [#336](https://github.com/Azure/ms-rest-js/pull/336)
|
||||||
|
|
||||||
## 1.7.0 - 2019-02-11
|
## 1.7.0 - 2019-02-11
|
||||||
|
|
||||||
- Added userAgentHeaderName to ServiceClientOptions [#330](https://github.com/Azure/ms-rest-js/pull/330)
|
- Added userAgentHeaderName to ServiceClientOptions [#330](https://github.com/Azure/ms-rest-js/pull/330)
|
||||||
|
|
||||||
## 1.6.0 - 2019-01-30
|
## 1.6.0 - 2019-01-30
|
||||||
|
|
||||||
- Fixed including proxy policy in browser [0c552f](https://github.com/Azure/ms-rest-js/commit/fafa26180e591db43d43c9cf0c7e93c8030c552f#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
|
- 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)
|
- Brought Axios interceptors back [c33602](https://github.com/Azure/ms-rest-js/commit/c1742fe6a80ed9b794115362633e0a8307c33602#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
|
||||||
|
|
||||||
## 1.5.2 - 2019-01-25
|
## 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 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)
|
- Added `@types/tunnel` [0865a2](https://github.com/Azure/ms-rest-js/commit/7a9b496d04294446f940f1549fb0a44dd9b94c01#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
|
||||||
|
|
||||||
## 1.5.1 - 2019-01-22
|
## 1.5.1 - 2019-01-22
|
||||||
|
|
||||||
- Fixed default HTTP client tests [c75b87](https://github.com/Azure/ms-rest-js/commit/4c2b1c5390deab989b5ec9cadb84891de9c75b87#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
|
- Fixed default HTTP client tests [c75b87](https://github.com/Azure/ms-rest-js/commit/4c2b1c5390deab989b5ec9cadb84891de9c75b87#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)
|
||||||
|
|
||||||
## 1.5.0 - 2019-01-15
|
## 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.
|
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:
|
- **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>
|
<pre>
|
||||||
------- (1) ----------------- (2) ------- (3) ------- (4) -------------- (5) ~~~~~~~
|
------- (1) ----------------- (2) ------- (3) ------- (4) -------------- (5) ~~~~~~~
|
||||||
|
|
|
@ -7,6 +7,7 @@ module.exports = function (config: any) {
|
||||||
plugins: [
|
plugins: [
|
||||||
"karma-mocha",
|
"karma-mocha",
|
||||||
"karma-chrome-launcher",
|
"karma-chrome-launcher",
|
||||||
|
"karma-firefox-launcher"
|
||||||
],
|
],
|
||||||
|
|
||||||
// frameworks to use
|
// frameworks to use
|
||||||
|
@ -56,6 +57,10 @@ module.exports = function (config: any) {
|
||||||
ChromeDebugging: {
|
ChromeDebugging: {
|
||||||
base: "Chrome",
|
base: "Chrome",
|
||||||
flags: [`http://localhost:${defaults.port}/debug.html`, "--auto-open-devtools-for-tabs", "--disable-web-security"]
|
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.
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
// 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
|
* @const
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
msRestVersion: "1.8.13",
|
msRestVersion: "2.0.0",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies HTTP.
|
* Specifies HTTP.
|
||||||
|
|
|
@ -28,8 +28,10 @@ export type TransferProgressEvent = {
|
||||||
*/
|
*/
|
||||||
export interface AbortSignalLike {
|
export interface AbortSignalLike {
|
||||||
readonly aborted: boolean;
|
readonly aborted: boolean;
|
||||||
addEventListener(type: "abort", listener: (this: AbortSignalLike, ev: any) => any, options?: any): void;
|
dispatchEvent: (event: Event) => boolean;
|
||||||
removeEventListener(type: "abort", listener: (this: AbortSignalLike, ev: any) => any, options?: any): void;
|
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;
|
withCredentials: boolean;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
proxySettings?: ProxySettings;
|
proxySettings?: ProxySettings;
|
||||||
|
keepAlive?: boolean;
|
||||||
|
|
||||||
abortSignal?: AbortSignalLike;
|
abortSignal?: AbortSignalLike;
|
||||||
|
|
||||||
|
@ -87,7 +90,8 @@ export class WebResource {
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
onUploadProgress?: (progress: TransferProgressEvent) => void,
|
onUploadProgress?: (progress: TransferProgressEvent) => void,
|
||||||
onDownloadProgress?: (progress: TransferProgressEvent) => void,
|
onDownloadProgress?: (progress: TransferProgressEvent) => void,
|
||||||
proxySettings?: ProxySettings) {
|
proxySettings?: ProxySettings,
|
||||||
|
keepAlive?: boolean) {
|
||||||
|
|
||||||
this.streamResponseBody = streamResponseBody;
|
this.streamResponseBody = streamResponseBody;
|
||||||
this.url = url || "";
|
this.url = url || "";
|
||||||
|
@ -102,6 +106,7 @@ export class WebResource {
|
||||||
this.onUploadProgress = onUploadProgress;
|
this.onUploadProgress = onUploadProgress;
|
||||||
this.onDownloadProgress = onDownloadProgress;
|
this.onDownloadProgress = onDownloadProgress;
|
||||||
this.proxySettings = proxySettings;
|
this.proxySettings = proxySettings;
|
||||||
|
this.keepAlive = keepAlive;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
21
package.json
21
package.json
|
@ -5,7 +5,7 @@
|
||||||
"email": "azsdkteam@microsoft.com",
|
"email": "azsdkteam@microsoft.com",
|
||||||
"url": "https://github.com/Azure/ms-rest-js"
|
"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",
|
"description": "Isomorphic client Runtime for Typescript/node.js/browser javascript client libraries generated using AutoRest",
|
||||||
"tags": [
|
"tags": [
|
||||||
"isomorphic",
|
"isomorphic",
|
||||||
|
@ -48,12 +48,14 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"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",
|
"form-data": "^2.3.2",
|
||||||
|
"node-fetch": "^2.6.0",
|
||||||
"tough-cookie": "^2.4.3",
|
"tough-cookie": "^2.4.3",
|
||||||
"tslib": "^1.9.2",
|
"tslib": "^1.9.2",
|
||||||
"tunnel": "0.0.6",
|
"tunnel": "0.0.6",
|
||||||
"@types/tunnel": "0.0.0",
|
|
||||||
"uuid": "^3.2.1",
|
"uuid": "^3.2.1",
|
||||||
"xml2js": "^0.4.19"
|
"xml2js": "^0.4.19"
|
||||||
},
|
},
|
||||||
|
@ -62,6 +64,7 @@
|
||||||
"@ts-common/azure-js-dev-tools": "^15.2.0",
|
"@ts-common/azure-js-dev-tools": "^15.2.0",
|
||||||
"@types/chai": "^4.1.7",
|
"@types/chai": "^4.1.7",
|
||||||
"@types/express": "^4.16.0",
|
"@types/express": "^4.16.0",
|
||||||
|
"@types/fetch-mock": "^7.2.5",
|
||||||
"@types/form-data": "^2.2.1",
|
"@types/form-data": "^2.2.1",
|
||||||
"@types/glob": "^7.1.1",
|
"@types/glob": "^7.1.1",
|
||||||
"@types/karma": "^3.0.0",
|
"@types/karma": "^3.0.0",
|
||||||
|
@ -75,13 +78,14 @@
|
||||||
"@types/webpack-dev-middleware": "^2.0.2",
|
"@types/webpack-dev-middleware": "^2.0.2",
|
||||||
"@types/xml2js": "^0.4.3",
|
"@types/xml2js": "^0.4.3",
|
||||||
"abortcontroller-polyfill": "^1.1.9",
|
"abortcontroller-polyfill": "^1.1.9",
|
||||||
"axios-mock-adapter": "^1.16.0",
|
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.3",
|
||||||
|
"fetch-mock": "^7.3.3",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"karma": "^4.1.0",
|
"karma": "^4.1.0",
|
||||||
"karma-chai": "^0.1.0",
|
"karma-chai": "^0.1.0",
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
"karma-firefox-launcher": "^1.1.0",
|
||||||
"karma-mocha": "^1.3.0",
|
"karma-mocha": "^1.3.0",
|
||||||
"karma-rollup-preprocessor": "^6.1.1",
|
"karma-rollup-preprocessor": "^6.1.1",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
|
@ -106,12 +110,12 @@
|
||||||
"semver": "^5.5.0",
|
"semver": "^5.5.0",
|
||||||
"shx": "^0.3.2",
|
"shx": "^0.3.2",
|
||||||
"sinon": "^7.1.1",
|
"sinon": "^7.1.1",
|
||||||
|
"terser": "^3.17.0",
|
||||||
"ts-loader": "^5.3.1",
|
"ts-loader": "^5.3.1",
|
||||||
"ts-node": "^7.0.0",
|
"ts-node": "^7.0.0",
|
||||||
"tslint": "^5.16.0",
|
"tslint": "^5.16.0",
|
||||||
"tslint-eslint-rules": "^5.4.0",
|
"tslint-eslint-rules": "^5.4.0",
|
||||||
"typescript": "^3.4.5",
|
"typescript": "^3.5.1",
|
||||||
"uglify-js": "^3.4.9",
|
|
||||||
"webpack": "^4.27.1",
|
"webpack": "^4.27.1",
|
||||||
"webpack-cli": "^3.1.2",
|
"webpack-cli": "^3.1.2",
|
||||||
"webpack-dev-middleware": "^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:lib": "run-s build:tsc build:rollup build:minify-browser",
|
||||||
"build:tsc": "tsc -p tsconfig.es.json",
|
"build:tsc": "tsc -p tsconfig.es.json",
|
||||||
"build:rollup": "rollup -c rollup.config.ts",
|
"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",
|
"build:test-browser": "webpack --config webpack.testconfig.ts",
|
||||||
"test": "run-p test:tslint test:unit test:karma",
|
"test": "run-p test:tslint test:unit test:karma",
|
||||||
"test:tslint": "tslint -p .",
|
"test:tslint": "tslint -p .",
|
||||||
"test:unit": "nyc mocha",
|
"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": "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",
|
"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",
|
"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",
|
"publish-preview": "mocha --no-colors && shx rm -rf dist/test && node ./.scripts/publish",
|
||||||
"local": "ts-node ./.scripts/local.ts",
|
"local": "ts-node ./.scripts/local.ts",
|
||||||
|
|
|
@ -23,8 +23,10 @@
|
||||||
const nodeConfig = {
|
const nodeConfig = {
|
||||||
input: "./es/lib/msRest.js",
|
input: "./es/lib/msRest.js",
|
||||||
external: [
|
external: [
|
||||||
"axios",
|
|
||||||
"form-data",
|
"form-data",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"node-fetch",
|
||||||
"os",
|
"os",
|
||||||
"stream",
|
"stream",
|
||||||
"tough-cookie",
|
"tough-cookie",
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import { assert, AssertionError } from "chai";
|
import { assert, AssertionError } from "chai";
|
||||||
import "chai/register-should";
|
import "chai/register-should";
|
||||||
import { createReadStream } from "fs";
|
import { createReadStream } from "fs";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import { DefaultHttpClient } from "../lib/defaultHttpClient";
|
import { DefaultHttpClient } from "../lib/defaultHttpClient";
|
||||||
import { RestError } from "../lib/restError";
|
import { RestError } from "../lib/restError";
|
||||||
|
@ -33,14 +32,14 @@ describe("defaultHttpClient", function () {
|
||||||
|
|
||||||
let httpMock: HttpMockFacade;
|
let httpMock: HttpMockFacade;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
httpMock = getHttpMock(axios);
|
httpMock = getHttpMock();
|
||||||
httpMock.setup();
|
httpMock.setup();
|
||||||
});
|
});
|
||||||
afterEach(() => httpMock.teardown());
|
afterEach(() => httpMock.teardown());
|
||||||
after(() => httpMock.teardown());
|
after(() => httpMock.teardown());
|
||||||
|
|
||||||
it("should return a response instead of throwing for awaited 404", async function () {
|
it("should return a response instead of throwing for awaited 404", async function () {
|
||||||
const resourceUrl = "/nonexistent/";
|
const resourceUrl = "/nonexistent";
|
||||||
|
|
||||||
httpMock.get(resourceUrl, async () => {
|
httpMock.get(resourceUrl, async () => {
|
||||||
return { status: 404 };
|
return { status: 404 };
|
||||||
|
@ -143,8 +142,8 @@ describe("defaultHttpClient", function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
it("for simple bodies", async function () {
|
it("for simple bodies", async function () {
|
||||||
httpMock.post("/fileupload", async (_url, _method, body) => {
|
httpMock.post("/fileupload", async (_url, _method, _body) => {
|
||||||
return { status: 251, body: body, headers: { "Content-Length": "200" } };
|
return { status: 251, body: body.repeat(9).substring(0, 200), headers: { "Content-Length": "200" } };
|
||||||
});
|
});
|
||||||
|
|
||||||
const upload: Notified = { notified: false };
|
const upload: Notified = { notified: false };
|
||||||
|
@ -173,14 +172,14 @@ describe("defaultHttpClient", function () {
|
||||||
|
|
||||||
const size = isNode ? payload.toString().length : undefined;
|
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 } };
|
return { status: 250, body: payload, headers: { "Content-Type": "text/javascript", "Content-length": size } };
|
||||||
});
|
});
|
||||||
|
|
||||||
const upload: Notified = { notified: false };
|
const upload: Notified = { notified: false };
|
||||||
const download: 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(upload, ev),
|
||||||
ev => listener(download, ev));
|
ev => listener(download, ev));
|
||||||
|
|
||||||
|
@ -212,7 +211,7 @@ describe("defaultHttpClient", function () {
|
||||||
await client.sendRequest(request);
|
await client.sendRequest(request);
|
||||||
throw new Error("request did not fail as expected");
|
throw new Error("request did not fail as expected");
|
||||||
} catch (err) {
|
} 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.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
import xhrMock, { proxy } from "xhr-mock";
|
import xhrMock, { proxy } from "xhr-mock";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import { isNode, HttpMethods } from "../lib/msRest";
|
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;
|
export type UrlFilter = string | RegExp;
|
||||||
|
|
||||||
|
@ -29,68 +29,77 @@ export interface HttpMockFacade {
|
||||||
put(url: UrlFilter, response: MockResponse): void;
|
put(url: UrlFilter, response: MockResponse): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHttpMock(axiosInstance?: AxiosInstance): HttpMockFacade {
|
export function getHttpMock(): HttpMockFacade {
|
||||||
return (isNode ? new NodeHttpMock(axiosInstance) : new BrowserHttpMock());
|
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 {
|
setup(): void {
|
||||||
this._mockAdapter.reset();
|
fetchMock.resetHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown(): void {
|
teardown(): void {
|
||||||
this._mockAdapter.restore();
|
fetchMock.resetHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
mockHttpMethod(method: HttpMethods, url: UrlFilter, response: MockResponse): void {
|
passThrough(_url?: string | RegExp | undefined): void {
|
||||||
const methodName = "on" + method.charAt(0) + method.slice(1).toLowerCase();
|
fetchMock.reset();
|
||||||
const mockCall: { reply: (statusOrCallback: number | Function, data?: any, headers?: any) => MockAdapter } = (this._mockAdapter as any)[methodName](url);
|
}
|
||||||
|
|
||||||
|
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") {
|
if (typeof response === "function") {
|
||||||
mockCall.reply(async (config: AxiosRequestConfig) => {
|
const mockFunction: MockResponseFunction = response;
|
||||||
const result = await response(config.url, config.method, config.data, config.headers);
|
mockResponse = (async (url: string, opts: any) => {
|
||||||
return [result.status, result.body, result.headers];
|
if (opts.body && typeof opts.body.pipe === "function") {
|
||||||
});
|
opts.body = await this.convertStreamToBuffer(opts.body);
|
||||||
} else {
|
|
||||||
mockCall.reply(response.status || 200, response.body || {}, response.headers || {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
get(url: UrlFilter, response: MockResponse): void {
|
||||||
return this.mockHttpMethod("GET", url, response);
|
this.mockHttpMethod("GET", url, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
post(url: UrlFilter, response: MockResponse): void {
|
post(url: UrlFilter, response: MockResponse): void {
|
||||||
return this.mockHttpMethod("POST", url, response);
|
this.mockHttpMethod("POST", url, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
put(url: UrlFilter, response: MockResponse): void {
|
put(url: UrlFilter, response: MockResponse): void {
|
||||||
return this.mockHttpMethod("PUT", url, response);
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrowserHttpMock implements HttpMockFacade {
|
export class BrowserHttpMock implements HttpMockFacade {
|
||||||
setup(): void {
|
setup(): void {
|
||||||
xhrMock.setup();
|
xhrMock.setup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ describe("MsRestUserAgentPolicy", () => {
|
||||||
|
|
||||||
it("should contain runtime information", async () => {
|
it("should contain runtime information", async () => {
|
||||||
const userAgent = await getUserAgent();
|
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 () => {
|
it("should have operating system information at the third place", async () => {
|
||||||
|
@ -149,7 +149,7 @@ describe("MsRestUserAgentPolicy", () => {
|
||||||
|
|
||||||
it("should contain runtime information", async () => {
|
it("should contain runtime information", async () => {
|
||||||
const userAgent = await getUserAgent();
|
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 () => {
|
it("should have operating system information at the second place", async () => {
|
||||||
|
|
|
@ -7,9 +7,9 @@ import tunnel from "tunnel";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
|
|
||||||
import { HttpHeaders } from "../lib/msRest";
|
import { HttpHeaders } from "../lib/msRest";
|
||||||
import { createTunnel, createProxyAgent } from "../lib/axiosHttpClient";
|
import { createProxyAgent, createTunnel } from "../lib/proxyAgent";
|
||||||
|
|
||||||
describe("AxiosHttpClient", () => {
|
describe("proxyAgent", () => {
|
||||||
describe("createProxyAgent", () => {
|
describe("createProxyAgent", () => {
|
||||||
type HttpsAgent = https.Agent & {
|
type HttpsAgent = https.Agent & {
|
||||||
defaultPort: number | undefined,
|
defaultPort: number | undefined,
|
|
@ -47,13 +47,11 @@ const config: webpack.Configuration = {
|
||||||
extensions: [".tsx", ".ts", ".js"]
|
extensions: [".tsx", ".ts", ".js"]
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
Buffer: "mock",
|
|
||||||
dns: false,
|
dns: false,
|
||||||
fs: "empty",
|
fs: "empty",
|
||||||
net: "empty",
|
net: "empty",
|
||||||
path: "empty",
|
path: "empty",
|
||||||
process: "mock",
|
process: "mock",
|
||||||
stream: "empty",
|
|
||||||
tls: "empty",
|
tls: "empty",
|
||||||
tty: false,
|
tty: false,
|
||||||
tunnel: "empty",
|
tunnel: "empty",
|
||||||
|
|
Загрузка…
Ссылка в новой задаче