diff --git a/packages/middleware-endpoint-discovery/LICENSE b/packages/middleware-endpoint-discovery/LICENSE new file mode 100644 index 000000000000..dd65ae06be7a --- /dev/null +++ b/packages/middleware-endpoint-discovery/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/middleware-endpoint-discovery/README.md b/packages/middleware-endpoint-discovery/README.md new file mode 100644 index 000000000000..4a50903c244f --- /dev/null +++ b/packages/middleware-endpoint-discovery/README.md @@ -0,0 +1,4 @@ +# @aws-sdk/middleware-endpoint-discovery + +[![NPM version](https://img.shields.io/npm/v/@aws-sdk/middleware-endpoint-discovery/latest.svg)](https://www.npmjs.com/package/@aws-sdk/middleware-endpoint-discovery) +[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/middleware-endpoint-discovery.svg)](https://www.npmjs.com/package/@aws-sdk/middleware-endpoint-discovery) diff --git a/packages/middleware-endpoint-discovery/jest.config.js b/packages/middleware-endpoint-discovery/jest.config.js new file mode 100644 index 000000000000..a8d1c2e49912 --- /dev/null +++ b/packages/middleware-endpoint-discovery/jest.config.js @@ -0,0 +1,5 @@ +const base = require("../../jest.config.base.js"); + +module.exports = { + ...base, +}; diff --git a/packages/middleware-endpoint-discovery/package.json b/packages/middleware-endpoint-discovery/package.json new file mode 100644 index 000000000000..7e33f416ca7a --- /dev/null +++ b/packages/middleware-endpoint-discovery/package.json @@ -0,0 +1,48 @@ +{ + "name": "@aws-sdk/middleware-endpoint-discovery", + "version": "3.0.0", + "scripts": { + "prepublishOnly": "yarn build && downlevel-dts dist/types dist/types/ts3.4", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build:es": "tsc -p tsconfig.es.json", + "build": "yarn build:es && yarn build:cjs", + "test": "jest" + }, + "main": "./dist/cjs/index.js", + "module": "./dist/es/index.js", + "types": "./dist/types/index.d.ts", + "author": { + "name": "AWS SDK for JavaScript Team", + "url": "https://aws.amazon.com/javascript/" + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-sdk/node-config-provider": "3.15.0", + "@types/jest": "^26.0.4", + "jest": "^26.1.0", + "typescript": "~4.2.4" + }, + "dependencies": { + "@aws-sdk/config-resolver": "3.15.0", + "@aws-sdk/endpoint-cache": "3.0.0", + "@aws-sdk/protocol-http": "3.15.0", + "@aws-sdk/types": "3.15.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "typesVersions": { + "<4.0": { + "types/*": [ + "types/ts3.4/*" + ] + } + }, + "homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/middleware-endpoint-discovery", + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-js-v3.git", + "directory": "packages/middleware-endpoint-discovery" + } +} diff --git a/packages/middleware-endpoint-discovery/src/configurations.spec.ts b/packages/middleware-endpoint-discovery/src/configurations.spec.ts new file mode 100644 index 000000000000..b1d1992c254d --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/configurations.spec.ts @@ -0,0 +1,85 @@ +import { NODE_ENDPOINT_DISCOVERY_CONFIG_OPTIONS } from "./configurations"; + +const { + environmentVariableSelector, + configFileSelector, + default: defaultSelector, +} = NODE_ENDPOINT_DISCOVERY_CONFIG_OPTIONS; + +describe("NODE_ENDPOINT_DISCOVERY_CONFIG_OPTIONS", () => { + describe("environmentVariableSelector", () => { + const ENV_ENDPOINT_DISCOVERY = ["AWS_ENABLE_ENDPOINT_DISCOVERY", "AWS_ENDPOINT_DISCOVERY_ENABLED"]; + describe.each(ENV_ENDPOINT_DISCOVERY)("env key: %p", (envKey) => { + const envValues = {}; + + beforeEach(() => { + ENV_ENDPOINT_DISCOVERY.forEach((envKey) => { + envValues[envKey] = process.env[envKey]; + delete process.env[envKey]; + }); + }); + + afterEach(() => { + ENV_ENDPOINT_DISCOVERY.forEach((envKey) => { + process.env[envKey] = envValues[envKey]; + delete envValues[envKey]; + }); + }); + + describe(`returns false`, () => { + it.each(["false", "0"])("if value stored is %s", (falsyValue) => { + process.env[envKey] = falsyValue; + expect(environmentVariableSelector(process.env)).toEqual(false); + }); + }); + + describe(`returns true`, () => { + it.each(["true", "1", "non-empty-value"])("if value stored is %s", (truthyValue) => { + process.env[envKey] = truthyValue; + expect(environmentVariableSelector(process.env)).toEqual(true); + }); + }); + + it(`returns undefined if value is not stored`, () => { + expect(environmentVariableSelector(process.env)).not.toBeDefined(); + }); + + it(`throws error if value stored is empty`, () => { + process.env[envKey] = ""; + expect(() => { + environmentVariableSelector(process.env); + }).toThrow(); + }); + }); + }); + + describe("configFileSelector", () => { + const CONFIG_ENDPOINT_DISCOVERY = "endpoint_discovery_enabled"; + + describe(`returns false`, () => { + it.each(["false", "0"])("if value stored is %s", (falsyValue) => { + expect(configFileSelector({ [CONFIG_ENDPOINT_DISCOVERY]: falsyValue })).toEqual(false); + }); + }); + + describe(`returns true`, () => { + it.each(["true", "1", "non-empty-value"])("if value stored is %s", (truthyValue) => { + expect(configFileSelector({ [CONFIG_ENDPOINT_DISCOVERY]: truthyValue })).toEqual(true); + }); + }); + + it(`returns undefined if value is not available`, () => { + expect(configFileSelector({})).not.toBeDefined(); + }); + + it(`throws if value stored is undefined`, () => { + expect(() => { + configFileSelector({ [CONFIG_ENDPOINT_DISCOVERY]: undefined }); + }).toThrow(); + }); + }); + + it("defaultSelector returns undefined", () => { + expect(defaultSelector).toBeUndefined(); + }); +}); diff --git a/packages/middleware-endpoint-discovery/src/configurations.ts b/packages/middleware-endpoint-discovery/src/configurations.ts new file mode 100644 index 000000000000..1a9b483aa45f --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/configurations.ts @@ -0,0 +1,32 @@ +import { LoadedConfigSelectors } from "@aws-sdk/node-config-provider"; + +const ENV_ENDPOINT_DISCOVERY = ["AWS_ENABLE_ENDPOINT_DISCOVERY", "AWS_ENDPOINT_DISCOVERY_ENABLED"]; +const CONFIG_ENDPOINT_DISCOVERY = "endpoint_discovery_enabled"; + +const isFalsy = (value: string) => ["false", "0"].indexOf(value) >= 0; + +export const NODE_ENDPOINT_DISCOVERY_CONFIG_OPTIONS: LoadedConfigSelectors = { + environmentVariableSelector: (env) => { + for (let i = 0; i < ENV_ENDPOINT_DISCOVERY.length; i++) { + const envKey = ENV_ENDPOINT_DISCOVERY[i]; + if (envKey in env) { + const value = env[envKey]; + if (value === "") { + throw Error(`Environment variable ${envKey} can't be empty of undefined, got "${value}"`); + } + // @ts-ignore Argument of type 'string | undefined' is not assignable to parameter of type 'string' + return !isFalsy(value); + } + } + }, + configFileSelector: (profile) => { + if (CONFIG_ENDPOINT_DISCOVERY in profile) { + const value = profile[CONFIG_ENDPOINT_DISCOVERY]; + if (value === undefined) { + throw Error(`Shared config entry ${CONFIG_ENDPOINT_DISCOVERY} can't be undefined, got "${value}"`); + } + return !isFalsy(value); + } + }, + default: undefined, +}; diff --git a/packages/middleware-endpoint-discovery/src/endpointDiscoveryMiddleware.spec.ts b/packages/middleware-endpoint-discovery/src/endpointDiscoveryMiddleware.spec.ts new file mode 100644 index 000000000000..0f267c0ca731 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/endpointDiscoveryMiddleware.spec.ts @@ -0,0 +1,145 @@ +import { EndpointCache } from "@aws-sdk/endpoint-cache"; +import { HttpRequest } from "@aws-sdk/protocol-http"; +import { BuildHandlerArguments, MiddlewareStack } from "@aws-sdk/types"; + +import { endpointDiscoveryMiddleware } from "./endpointDiscoveryMiddleware"; +import { getCacheKey } from "./getCacheKey"; +import { updateDiscoveredEndpointInCache } from "./updateDiscoveredEndpointInCache"; + +jest.mock("./updateDiscoveredEndpointInCache"); +jest.mock("./getCacheKey"); +jest.mock("@aws-sdk/protocol-http"); + +describe(endpointDiscoveryMiddleware.name, () => { + const cacheKey = "cacheKey"; + const endpoint = "endpoint"; + const getEndpoint = jest.fn().mockReturnValue(endpoint); + const mockConfig = { + credentials: jest.fn(), + endpointCache: ({ + getEndpoint, + } as unknown) as EndpointCache, + endpointDiscoveryEnabled: jest.fn().mockResolvedValue(undefined), + endpointDiscoveryEnabledProvider: jest.fn(), + endpointDiscoveryCommandCtor: jest.fn(), + isCustomEndpoint: false, + isClientEndpointDiscoveryEnabled: false, + }; + + const mockMiddlewareConfig = { + isDiscoveredEndpointRequired: false, + clientStack: {} as MiddlewareStack, + }; + + const mockNext = jest.fn(); + const mockContext = { + clientName: "ExampleClient", + commandName: "ExampleCommand", + }; + const mockArgs = { request: {} }; + + beforeEach(() => { + (getCacheKey as jest.Mock).mockResolvedValue(cacheKey); + (updateDiscoveredEndpointInCache as jest.Mock).mockResolvedValue(undefined); + const { isInstance } = HttpRequest; + ((isInstance as unknown) as jest.Mock).mockReturnValue(true); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe(`isCustomEndpoint=true`, () => { + it(`throw error if isClientEndpointDiscoveryEnabled`, async () => { + try { + await endpointDiscoveryMiddleware( + { ...mockConfig, isCustomEndpoint: true, isClientEndpointDiscoveryEnabled: true }, + mockMiddlewareConfig + )( + mockNext, + mockContext + )(mockArgs as BuildHandlerArguments); + fail("should throw error when isCustomEndpoint=true and isClientEndpointDiscoveryEnabled=true"); + } catch (error) { + expect(error).toStrictEqual( + new Error(`Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.`) + ); + } + expect(mockNext).not.toHaveBeenCalled(); + expect(updateDiscoveredEndpointInCache).not.toHaveBeenCalled(); + }); + + it(`returns without endpoint discovery`, async () => { + await endpointDiscoveryMiddleware({ ...mockConfig, isCustomEndpoint: true }, mockMiddlewareConfig)( + mockNext, + mockContext + )(mockArgs as BuildHandlerArguments); + expect(mockNext).toHaveBeenCalledWith(mockArgs); + expect(updateDiscoveredEndpointInCache as jest.Mock).not.toHaveBeenCalled(); + }); + }); + + describe(`isDiscoveredEndpointRequired=true`, () => { + it(`throws error when isEndpointDiscoveryEnabled=false`, async () => { + mockConfig.endpointDiscoveryEnabled.mockResolvedValueOnce(false); + try { + await endpointDiscoveryMiddleware(mockConfig, { ...mockMiddlewareConfig, isDiscoveredEndpointRequired: true })( + mockNext, + mockContext + )(mockArgs as BuildHandlerArguments); + fail("should throw error when isDiscoveredEndpointRequired=true and isEndpointDiscoveryEnabled=false"); + } catch (error) { + expect(error).toStrictEqual( + new Error( + `Endpoint Discovery is disabled but ${mockContext.commandName} on ${mockContext.clientName} requires it.` + + ` Please check your configurations.` + ) + ); + } + expect(mockNext).not.toHaveBeenCalled(); + expect(updateDiscoveredEndpointInCache).not.toHaveBeenCalled(); + }); + + describe(`calls updateDiscoveredEndpointInCache`, () => { + it(`when isEndpointDiscoveryEnabled=undefined`, async () => { + await endpointDiscoveryMiddleware(mockConfig, { ...mockMiddlewareConfig, isDiscoveredEndpointRequired: true })( + mockNext, + mockContext + )(mockArgs as BuildHandlerArguments); + expect(mockNext).toHaveBeenCalledWith(mockArgs); + expect(mockNext).toHaveBeenCalledWith({ request: { hostname: endpoint } }); + expect(updateDiscoveredEndpointInCache).toHaveBeenCalled(); + }); + + it(`when isEndpointDiscoveryEnabled=true`, async () => { + mockConfig.endpointDiscoveryEnabled.mockResolvedValueOnce(true); + await endpointDiscoveryMiddleware(mockConfig, { ...mockMiddlewareConfig, isDiscoveredEndpointRequired: true })( + mockNext, + mockContext + )(mockArgs as BuildHandlerArguments); + expect(mockNext).toHaveBeenCalledWith(mockArgs); + expect(mockNext).toHaveBeenCalledWith({ request: { hostname: endpoint } }); + expect(updateDiscoveredEndpointInCache).toHaveBeenCalled(); + }); + }); + + describe(`isDiscoveredEndpointRequired=false`, () => { + it(`calls updateDiscoveredEndpointInCache when isEndpointDiscoveryEnabled=true`, async () => { + mockConfig.endpointDiscoveryEnabled.mockResolvedValueOnce(true); + await endpointDiscoveryMiddleware(mockConfig, mockMiddlewareConfig)(mockNext, mockContext)( + mockArgs as BuildHandlerArguments + ); + expect(mockNext).toHaveBeenCalledWith(mockArgs); + expect(updateDiscoveredEndpointInCache).toHaveBeenCalled(); + }); + + it(`does not call updateDiscoveredEndpointInCache when isEndpointDiscoveryEnabled=false`, async () => { + await endpointDiscoveryMiddleware(mockConfig, mockMiddlewareConfig)(mockNext, mockContext)( + mockArgs as BuildHandlerArguments + ); + expect(mockNext).toHaveBeenCalledWith(mockArgs); + expect(updateDiscoveredEndpointInCache).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/packages/middleware-endpoint-discovery/src/endpointDiscoveryMiddleware.ts b/packages/middleware-endpoint-discovery/src/endpointDiscoveryMiddleware.ts new file mode 100644 index 000000000000..28647a67e958 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/endpointDiscoveryMiddleware.ts @@ -0,0 +1,72 @@ +import { HttpRequest } from "@aws-sdk/protocol-http"; +import { + BuildHandler, + BuildHandlerArguments, + BuildHandlerOutput, + HandlerExecutionContext, + MetadataBearer, +} from "@aws-sdk/types"; + +import { getCacheKey } from "./getCacheKey"; +import { EndpointDiscoveryMiddlewareConfig } from "./getEndpointDiscoveryPlugin"; +import { EndpointDiscoveryResolvedConfig, PreviouslyResolved } from "./resolveEndpointDiscoveryConfig"; +import { updateDiscoveredEndpointInCache } from "./updateDiscoveredEndpointInCache"; + +export const endpointDiscoveryMiddleware = ( + config: EndpointDiscoveryResolvedConfig & PreviouslyResolved, + middlewareConfig: EndpointDiscoveryMiddlewareConfig +) => ( + next: BuildHandler, + context: HandlerExecutionContext +): BuildHandler => async (args: BuildHandlerArguments): Promise> => { + if (config.isCustomEndpoint) { + if (config.isClientEndpointDiscoveryEnabled) { + throw new Error(`Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.`); + } + return next(args); + } + + const { endpointDiscoveryCommandCtor } = config; + const { isDiscoveredEndpointRequired, identifiers } = middlewareConfig; + const { clientName, commandName } = context; + const isEndpointDiscoveryEnabled = await config.endpointDiscoveryEnabled(); + const cacheKey = await getCacheKey(commandName, config, { identifiers }); + + if (isDiscoveredEndpointRequired) { + // throw error if endpoint discovery is required, and it's explicitly disabled. + if (isEndpointDiscoveryEnabled === false) { + throw new Error( + `Endpoint Discovery is disabled but ${commandName} on ${clientName} requires it.` + + ` Please check your configurations.` + ); + } + // call await on Endpoint Discovery API utility so that function blocks + // till discovered endpoint is updated in cache + await updateDiscoveredEndpointInCache(config, { + ...middlewareConfig, + commandName, + cacheKey, + endpointDiscoveryCommandCtor, + }); + } else if (isEndpointDiscoveryEnabled) { + // Discover endpoints only if endpoint discovery is explicitly enabled. + // Do not call await await on Endpoint Discovery API utility so that function + // does not block, the command will use discovered endpoint, if available. + updateDiscoveredEndpointInCache(config, { + ...middlewareConfig, + commandName, + cacheKey, + endpointDiscoveryCommandCtor, + }); + } + + const { request } = args; + if (cacheKey && HttpRequest.isInstance(request)) { + const endpoint = config.endpointCache.getEndpoint(cacheKey); + if (endpoint) { + request.hostname = endpoint; + } + } + + return next(args); +}; diff --git a/packages/middleware-endpoint-discovery/src/getCacheKey.spec.ts b/packages/middleware-endpoint-discovery/src/getCacheKey.spec.ts new file mode 100644 index 000000000000..03d4c72811fd --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/getCacheKey.spec.ts @@ -0,0 +1,32 @@ +import { getCacheKey } from "./getCacheKey"; + +describe(getCacheKey.name, () => { + const commandName = "commandName"; + const mockCredentials = { + accessKeyId: "accessKeyId", + secretAccessKey: "secretAccessKey", + }; + + const config = { + credentials: () => Promise.resolve(mockCredentials), + }; + + it("returns accessKeyId in cacheKey", async () => { + const cacheKey = await getCacheKey(commandName, config, {}); + expect(cacheKey).toEqual(JSON.stringify({ accessKeyId: mockCredentials.accessKeyId })); + }); + + it("returns commandName and identifiers if passed", async () => { + const identifiers = { key: "value" }; + const cacheKey = await getCacheKey(commandName, config, { identifiers }); + expect(cacheKey).toEqual(JSON.stringify({ accessKeyId: mockCredentials.accessKeyId, commandName, identifiers })); + }); + + it("returns same cache key irrespective of key order in identifiers", async () => { + const identifiers1 = { key1: "value1", key2: "value2" }; + const cacheKey1 = await getCacheKey(commandName, config, { identifiers: identifiers1 }); + const identifiers2 = { key2: "value2", key1: "value1" }; + const cacheKey2 = await getCacheKey(commandName, config, { identifiers: identifiers2 }); + expect(cacheKey1).toStrictEqual(cacheKey2); + }); +}); diff --git a/packages/middleware-endpoint-discovery/src/getCacheKey.ts b/packages/middleware-endpoint-discovery/src/getCacheKey.ts new file mode 100644 index 000000000000..09b36a098425 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/getCacheKey.ts @@ -0,0 +1,24 @@ +import { Credentials, Provider } from "@aws-sdk/types"; + +/** + * Generate key to index the endpoints in the cache + */ +export const getCacheKey = async ( + commandName: string, + config: { credentials: Provider }, + options: { + identifiers?: { [key: string]: string }; + } +) => { + const { accessKeyId } = await config.credentials(); + const { identifiers } = options; + return JSON.stringify({ + ...(accessKeyId && { accessKeyId }), + ...(identifiers && { + commandName, + identifiers: Object.entries(identifiers) + .sort() + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + }), + }); +}; diff --git a/packages/middleware-endpoint-discovery/src/getEndpointDiscoveryPlugin.spec.ts b/packages/middleware-endpoint-discovery/src/getEndpointDiscoveryPlugin.spec.ts new file mode 100644 index 000000000000..a85937e5a77b --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/getEndpointDiscoveryPlugin.spec.ts @@ -0,0 +1,32 @@ +import { endpointDiscoveryMiddleware } from "./endpointDiscoveryMiddleware"; +import { endpointDiscoveryMiddlewareOptions, getEndpointDiscoveryPlugin } from "./getEndpointDiscoveryPlugin"; + +jest.mock("./endpointDiscoveryMiddleware"); + +describe(getEndpointDiscoveryPlugin.name, () => { + const pluginConfig = { + isCustomEndpoint: false, + endpointCache: jest.fn(), + endpointDiscoveryEnabled: jest.fn(), + isClientEndpointDiscoveryEnabled: false, + }; + const middlewareConfig = { + isDiscoveredEndpointRequired: false, + }; + + it(`applyToStack function adds endpoint discovery middleware`, () => { + const middlewareReturn = {}; + (endpointDiscoveryMiddleware as jest.Mock).mockReturnValueOnce(middlewareReturn); + + // @ts-ignore + const plugin = getEndpointDiscoveryPlugin(pluginConfig, middlewareConfig); + const commandStack = { add: jest.fn() }; + + // @ts-ignore + plugin.applyToStack(commandStack); + expect(commandStack.add).toHaveBeenCalled(); + expect(commandStack.add).toHaveBeenCalledWith(middlewareReturn, endpointDiscoveryMiddlewareOptions); + expect(endpointDiscoveryMiddleware).toHaveBeenCalled(); + expect(endpointDiscoveryMiddleware).toHaveBeenCalledWith(pluginConfig, middlewareConfig); + }); +}); diff --git a/packages/middleware-endpoint-discovery/src/getEndpointDiscoveryPlugin.ts b/packages/middleware-endpoint-discovery/src/getEndpointDiscoveryPlugin.ts new file mode 100644 index 000000000000..10383983bb04 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/getEndpointDiscoveryPlugin.ts @@ -0,0 +1,51 @@ +import { BuildHandlerOptions, HttpHandlerOptions, MiddlewareStack, Pluggable } from "@aws-sdk/types"; + +import { endpointDiscoveryMiddleware } from "./endpointDiscoveryMiddleware"; +import { EndpointDiscoveryResolvedConfig, PreviouslyResolved } from "./resolveEndpointDiscoveryConfig"; + +export const endpointDiscoveryMiddlewareOptions: BuildHandlerOptions = { + name: "endpointDiscoveryMiddleware", + step: "build", + tags: ["ENDPOINT_DISCOVERY"], + override: true, +}; + +export interface EndpointDiscoveryMiddlewareConfig { + isDiscoveredEndpointRequired: boolean; + clientStack: MiddlewareStack; + options?: HttpHandlerOptions; + identifiers?: { [key: string]: string }; +} + +export const getEndpointDiscoveryPlugin = ( + pluginConfig: EndpointDiscoveryResolvedConfig & PreviouslyResolved, + middlewareConfig: EndpointDiscoveryMiddlewareConfig +): Pluggable => ({ + applyToStack: (commandStack) => { + commandStack.add(endpointDiscoveryMiddleware(pluginConfig, middlewareConfig), endpointDiscoveryMiddlewareOptions); + }, +}); + +export const getEndpointDiscoveryRequiredPlugin = ( + pluginConfig: EndpointDiscoveryResolvedConfig & PreviouslyResolved, + middlewareConfig: Omit +): Pluggable => ({ + applyToStack: (commandStack) => { + commandStack.add( + endpointDiscoveryMiddleware(pluginConfig, { ...middlewareConfig, isDiscoveredEndpointRequired: true }), + endpointDiscoveryMiddlewareOptions + ); + }, +}); + +export const getEndpointDiscoveryOptionalPlugin = ( + pluginConfig: EndpointDiscoveryResolvedConfig & PreviouslyResolved, + middlewareConfig: Omit +): Pluggable => ({ + applyToStack: (commandStack) => { + commandStack.add( + endpointDiscoveryMiddleware(pluginConfig, { ...middlewareConfig, isDiscoveredEndpointRequired: false }), + endpointDiscoveryMiddlewareOptions + ); + }, +}); diff --git a/packages/middleware-endpoint-discovery/src/index.ts b/packages/middleware-endpoint-discovery/src/index.ts new file mode 100644 index 000000000000..4724fd5a5bc0 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/index.ts @@ -0,0 +1,3 @@ +export * from "./resolveEndpointDiscoveryConfig"; +export * from "./getEndpointDiscoveryPlugin"; +export * from "./configurations"; diff --git a/packages/middleware-endpoint-discovery/src/resolveEndpointDiscoveryConfig.spec.ts b/packages/middleware-endpoint-discovery/src/resolveEndpointDiscoveryConfig.spec.ts new file mode 100644 index 000000000000..f0d554a2304c --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/resolveEndpointDiscoveryConfig.spec.ts @@ -0,0 +1,63 @@ +import { EndpointCache } from "@aws-sdk/endpoint-cache"; + +import { resolveEndpointDiscoveryConfig } from "./resolveEndpointDiscoveryConfig"; + +jest.mock("@aws-sdk/endpoint-cache"); + +describe(resolveEndpointDiscoveryConfig.name, () => { + const endpointDiscoveryCommandCtor = jest.fn(); + const mockInput = { + isCustomEndpoint: false, + credentials: jest.fn(), + endpointDiscoveryEnabledProvider: jest.fn(), + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("assigns endpointDiscoveryCommandCtor in resolvedConfig", () => { + const resolvedConfig = resolveEndpointDiscoveryConfig(mockInput, endpointDiscoveryCommandCtor); + expect(resolvedConfig.endpointDiscoveryCommandCtor).toStrictEqual(endpointDiscoveryCommandCtor); + }); + + describe("endpointCache", () => { + it("creates cache of size endpointCacheSize if passed", () => { + const endpointCacheSize = 100; + resolveEndpointDiscoveryConfig( + { + ...mockInput, + endpointCacheSize, + }, + endpointDiscoveryCommandCtor + ); + expect(EndpointCache).toBeCalledWith(endpointCacheSize); + }); + + it("creates cache of size 1000 if endpointCacheSize not passed", () => { + resolveEndpointDiscoveryConfig(mockInput, endpointDiscoveryCommandCtor); + expect(EndpointCache).toBeCalledWith(1000); + }); + }); + + describe("endpointDiscoveryEnabled", () => { + it.each([false, true])(`sets to value passed in the config: %s`, (endpointDiscoveryEnabled) => { + const resolvedConfig = resolveEndpointDiscoveryConfig( + { + ...mockInput, + endpointDiscoveryEnabled, + }, + endpointDiscoveryCommandCtor + ); + expect(resolvedConfig.endpointDiscoveryEnabled()).resolves.toBe(endpointDiscoveryEnabled); + expect(mockInput.endpointDiscoveryEnabledProvider).not.toHaveBeenCalled(); + expect(resolvedConfig.isClientEndpointDiscoveryEnabled).toStrictEqual(true); + }); + + it(`sets to endpointDiscoveryEnabledProvider if value is not passed`, () => { + const resolvedConfig = resolveEndpointDiscoveryConfig(mockInput, endpointDiscoveryCommandCtor); + expect(resolvedConfig.endpointDiscoveryEnabled).toBe(mockInput.endpointDiscoveryEnabledProvider); + expect(resolvedConfig.isClientEndpointDiscoveryEnabled).toStrictEqual(false); + }); + }); +}); diff --git a/packages/middleware-endpoint-discovery/src/resolveEndpointDiscoveryConfig.ts b/packages/middleware-endpoint-discovery/src/resolveEndpointDiscoveryConfig.ts new file mode 100644 index 000000000000..6d702544b0d9 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/resolveEndpointDiscoveryConfig.ts @@ -0,0 +1,66 @@ +import { EndpointCache } from "@aws-sdk/endpoint-cache"; +import { Credentials, Provider } from "@aws-sdk/types"; + +export interface EndpointDiscoveryInputConfig {} + +export interface PreviouslyResolved { + isCustomEndpoint: boolean; + credentials: Provider; + endpointDiscoveryEnabledProvider: Provider; +} + +export interface EndpointDiscoveryInputConfig { + /** + * The size of the client cache storing endpoints from endpoint discovery operations. + * Defaults to 1000. + */ + endpointCacheSize?: number; + + /** + * Whether to call operations with endpoints given by service dynamically. + * Setting this config to `true` will enable endpoint discovery for all applicable operations. + * Setting it to `false` will explicitly disable endpoint discovery even though operations that + * require endpoint discovery will presumably fail. Leaving it to undefined means SDK only do + * endpoint discovery when it's required. Defaults to `undefined`. + */ + endpointDiscoveryEnabled?: boolean | undefined; +} +export interface EndpointDiscoveryResolvedConfig { + /** + * LRU Cache which stores endpoints from endpoint discovery operations. + * The size is either provided by {@link EndpointDiscoveryInputConfig.endpointCacheSize}. + */ + endpointCache: EndpointCache; + + /** + * The constructor of the Command used for discovering endpoints. + * @internal + */ + endpointDiscoveryCommandCtor: new (comandConfig: any) => any; + + /** + * Resolved value for input config {@link EndpointDiscoveryInputConfig.endpointDiscoveryEnabled}. + */ + endpointDiscoveryEnabled: Provider; + + /** + * Stores whether endpoint discovery configuration is set locally by passing + * {@link EndpointDiscoveryInputConfig.endpointDiscoveryEnabled} during client creation. + * @internal + */ + isClientEndpointDiscoveryEnabled: boolean; +} + +export const resolveEndpointDiscoveryConfig = ( + input: T & PreviouslyResolved & EndpointDiscoveryInputConfig, + endpointDiscoveryCommandCtor: new (comandConfig: any) => any +): T & EndpointDiscoveryResolvedConfig => ({ + ...input, + endpointDiscoveryCommandCtor, + endpointCache: new EndpointCache(input.endpointCacheSize ?? 1000), + endpointDiscoveryEnabled: + input.endpointDiscoveryEnabled !== undefined + ? () => Promise.resolve(input.endpointDiscoveryEnabled) + : input.endpointDiscoveryEnabledProvider, + isClientEndpointDiscoveryEnabled: input.endpointDiscoveryEnabled !== undefined, +}); diff --git a/packages/middleware-endpoint-discovery/src/updateDiscoveredEndpointInCache.spec.ts b/packages/middleware-endpoint-discovery/src/updateDiscoveredEndpointInCache.spec.ts new file mode 100644 index 000000000000..84473c620c15 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/updateDiscoveredEndpointInCache.spec.ts @@ -0,0 +1,172 @@ +import { updateDiscoveredEndpointInCache } from "./updateDiscoveredEndpointInCache"; + +describe(updateDiscoveredEndpointInCache.name, () => { + const cacheKey = "cacheKey"; + const mockGet = jest.fn(); + const mockSet = jest.fn(); + const mockDelete = jest.fn(); + + const mockHandler = jest.fn(); + const mockResolveMiddleware = jest.fn().mockReturnValue(mockHandler); + + const mockEndpoints = [{ Address: "mockAddress", CachePeriodInMinutes: 2 }]; + const placeholderEndpoints = [{ Address: "", CachePeriodInMinutes: 1 }]; + + const config = { + endpointCache: { get: mockGet, set: mockSet, delete: mockDelete }, + }; + + const options = { + cacheKey, + commandName: "ExampleCommand", + endpointDiscoveryCommandCtor: jest.fn().mockReturnValue({ resolveMiddleware: mockResolveMiddleware }), + isDiscoveredEndpointRequired: false, + identifiers: { key: "value" }, + }; + + beforeEach(() => { + mockGet.mockReturnValue(mockEndpoints); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it(`returns if endpoints are present in cacheKey`, async () => { + // @ts-ignore + await updateDiscoveredEndpointInCache(config, options); + expect(mockGet).toHaveBeenCalledWith(cacheKey); + expect(mockSet).not.toHaveBeenCalled(); + expect(mockDelete).not.toHaveBeenCalled(); + }); + + describe("calls endpointDiscoveryCommandCtor", () => { + beforeEach(() => { + mockGet.mockReturnValue(undefined); + }); + + const verifyCallsOnCacheUndefined = () => { + expect(mockGet).toHaveBeenCalledTimes(1); + + expect(options.endpointDiscoveryCommandCtor).toHaveBeenCalledWith({ + Operation: options.commandName.substr(0, options.commandName.length - 7), + Identifiers: options.identifiers, + }); + expect(mockHandler).toHaveBeenCalledTimes(1); + }; + + it("on successful call: updates cache", async () => { + mockHandler.mockResolvedValueOnce({ output: { Endpoints: mockEndpoints } }); + + // @ts-ignore + await updateDiscoveredEndpointInCache(config, options); + + verifyCallsOnCacheUndefined(); + expect(mockDelete).not.toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledTimes(2); + expect(mockSet).toHaveBeenNthCalledWith(1, cacheKey, placeholderEndpoints); + expect(mockSet).toHaveBeenNthCalledWith(2, cacheKey, mockEndpoints); + }); + + it("calls endpointDiscoveryCommandCtor command just once in case of parallel calls", async () => { + mockHandler.mockResolvedValueOnce({ output: { Endpoints: mockEndpoints } }); + // First call returns undefined, while other ones return placeholder endpoints a call is in progress + mockGet + .mockReturnValueOnce(undefined) + .mockReturnValueOnce(placeholderEndpoints) + .mockReturnValueOnce(placeholderEndpoints); + + await Promise.all([ + // @ts-ignore + updateDiscoveredEndpointInCache(config, options), + // @ts-ignore + updateDiscoveredEndpointInCache(config, options), + // @ts-ignore + updateDiscoveredEndpointInCache(config, options), + ]); + + expect(mockGet).toHaveBeenCalledTimes(3); + + expect(options.endpointDiscoveryCommandCtor).toHaveBeenCalledWith({ + Operation: options.commandName.substr(0, options.commandName.length - 7), + Identifiers: options.identifiers, + }); + expect(mockHandler).toHaveBeenCalledTimes(1); + + expect(mockDelete).not.toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledTimes(2); + expect(mockSet).toHaveBeenNthCalledWith(1, cacheKey, placeholderEndpoints); + expect(mockSet).toHaveBeenNthCalledWith(2, cacheKey, mockEndpoints); + }); + + describe("on error", () => { + it(`throws if isDiscoveredEndpointRequired=true`, async () => { + const error = new Error("rejected"); + mockHandler.mockRejectedValueOnce(error); + + try { + // @ts-ignore + await updateDiscoveredEndpointInCache(config, { ...options, isDiscoveredEndpointRequired: true }); + fail("updateDiscoveredEndpointInCache should throw"); + } catch (error) { + expect(error).toEqual( + Object.assign( + new Error( + `The operation to discover endpoint failed.` + + ` Please retry, or provide a custom endpoint and disable endpoint discovery to proceed.` + ), + { reason: error } + ) + ); + } + verifyCallsOnCacheUndefined(); + expect(mockDelete).not.toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledTimes(1); + expect(mockSet).toHaveBeenCalledWith(cacheKey, placeholderEndpoints); + }); + + it(`sets placeholder enpoint if isDiscoveredEndpointRequired=false`, async () => { + const error = new Error("rejected"); + mockHandler.mockRejectedValueOnce(error); + + // @ts-ignore + await updateDiscoveredEndpointInCache(config, options); + + verifyCallsOnCacheUndefined(); + expect(mockDelete).not.toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledTimes(2); + expect(mockSet).toHaveBeenNthCalledWith(1, cacheKey, placeholderEndpoints); + expect(mockSet).toHaveBeenNthCalledWith(2, cacheKey, placeholderEndpoints); + }); + + describe(`deletes cacheKey in case of`, () => { + const verifyCallsOnCacheKeyDelete = () => { + expect(mockDelete).toHaveBeenCalledWith(cacheKey); + expect(mockSet).toHaveBeenCalledTimes(2); + expect(mockSet).toHaveBeenNthCalledWith(1, cacheKey, placeholderEndpoints); + expect(mockSet).toHaveBeenNthCalledWith(2, cacheKey, placeholderEndpoints); + }; + + it(`InvalidEndpointException`, async () => { + const error = Object.assign(new Error("Invalid endpoint!"), { name: "InvalidEndpointException" }); + mockHandler.mockRejectedValueOnce(error); + + // @ts-ignore + await updateDiscoveredEndpointInCache(config, options); + verifyCallsOnCacheUndefined(); + verifyCallsOnCacheKeyDelete(); + }); + + it(`Status code: 421`, async () => { + const error = Object.assign(new Error("Invalid endpoint!"), { $metadata: { httpStatusCode: 421 } }); + mockHandler.mockRejectedValueOnce(error); + + // @ts-ignore + await updateDiscoveredEndpointInCache(config, options); + verifyCallsOnCacheUndefined(); + verifyCallsOnCacheKeyDelete(); + }); + }); + }); + }); +}); diff --git a/packages/middleware-endpoint-discovery/src/updateDiscoveredEndpointInCache.ts b/packages/middleware-endpoint-discovery/src/updateDiscoveredEndpointInCache.ts new file mode 100644 index 000000000000..117c3b9109a1 --- /dev/null +++ b/packages/middleware-endpoint-discovery/src/updateDiscoveredEndpointInCache.ts @@ -0,0 +1,88 @@ +import { EndpointDiscoveryMiddlewareConfig } from "./getEndpointDiscoveryPlugin"; +import { EndpointDiscoveryResolvedConfig, PreviouslyResolved } from "./resolveEndpointDiscoveryConfig"; + +export interface UpdateDiscoveredEndpointInCacheOptions extends EndpointDiscoveryMiddlewareConfig { + cacheKey: string; + commandName: string; + endpointDiscoveryCommandCtor: new (comandConfig: any) => any; +} + +const requestQueue: { [key: string]: { resolve: Function; reject: Function }[] } = {}; + +export const updateDiscoveredEndpointInCache = async ( + config: EndpointDiscoveryResolvedConfig & PreviouslyResolved, + options: UpdateDiscoveredEndpointInCacheOptions +) => + new Promise((resolve, reject) => { + const { endpointCache } = config; + const { cacheKey, commandName, identifiers } = options; + + const endpoints = endpointCache.get(cacheKey); + + if (endpoints && endpoints.length === 1 && endpoints[0].Address === "") { + // Endpoint operation already in-flight. + // Add request to request queue only if discovered endpoint is required. + if (options.isDiscoveredEndpointRequired) { + if (!requestQueue[cacheKey]) requestQueue[cacheKey] = []; + requestQueue[cacheKey].push({ resolve, reject }); + } else { + resolve(); + } + } else if (endpoints && endpoints.length > 0) { + // Endpoint record is present in cache. + resolve(); + } else { + // put in a placeholder for endpoints already requested, prevent + // too much in-flight calls. + const placeholderEndpoints = [{ Address: "", CachePeriodInMinutes: 1 }]; + endpointCache.set(cacheKey, placeholderEndpoints); + + const command = new options.endpointDiscoveryCommandCtor({ + Operation: commandName.substr(0, commandName.length - 7), // strip "Command" + Identifiers: identifiers, + }); + const handler = command.resolveMiddleware(options.clientStack, config, options.options); + handler(command) + .then((result: any) => { + endpointCache.set(cacheKey, result.output.Endpoints); + if (requestQueue[cacheKey]) { + requestQueue[cacheKey].forEach(({ resolve }) => { + resolve(); + }); + delete requestQueue[cacheKey]; + } + resolve(); + }) + .catch((error: any) => { + if (error.name === "InvalidEndpointException" || error.$metadata?.httpStatusCode === 421) { + // Endpoint is invalid, delete the cache entry. + endpointCache.delete(cacheKey); + } + + const errorToThrow = Object.assign( + new Error( + `The operation to discover endpoint failed.` + + ` Please retry, or provide a custom endpoint and disable endpoint discovery to proceed.` + ), + { reason: error } + ); + + //fail all the pending requests in batch + if (requestQueue[cacheKey]) { + requestQueue[cacheKey].forEach(({ reject }) => { + reject(errorToThrow); + }); + delete requestQueue[cacheKey]; + } + + if (options.isDiscoveredEndpointRequired) { + reject(errorToThrow); + } else { + // Endpoint Discovery is optional. No error needs to be thrown. + // Set placeHolder endpoint to disable refresh for one minute. + endpointCache.set(cacheKey, placeholderEndpoints); + resolve(); + } + }); + } + }); diff --git a/packages/middleware-endpoint-discovery/tsconfig.cjs.json b/packages/middleware-endpoint-discovery/tsconfig.cjs.json new file mode 100644 index 000000000000..67f09ddd4138 --- /dev/null +++ b/packages/middleware-endpoint-discovery/tsconfig.cjs.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "declarationDir": "./dist/types", + "rootDir": "./src", + "outDir": "./dist/cjs", + "baseUrl": "." + }, + "extends": "../../tsconfig.cjs.json", + "include": ["src/"] +} diff --git a/packages/middleware-endpoint-discovery/tsconfig.es.json b/packages/middleware-endpoint-discovery/tsconfig.es.json new file mode 100644 index 000000000000..6adc0089d91d --- /dev/null +++ b/packages/middleware-endpoint-discovery/tsconfig.es.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "lib": ["es5", "es2015.promise", "es2015.collection", "es2015.iterable", "es2015.symbol.wellknown"], + "declarationDir": "./dist/types", + "rootDir": "./src", + "outDir": "./dist/es", + "baseUrl": "." + }, + "extends": "../../tsconfig.es.json", + "include": ["src/"] +}