Repository: aws/aws-sdk-js
Pull request #2253 (2018-09-11 20:57:44)

Endpoint Discovery Feature
Add automatic endpoint discovery. Vendor the endpoint cache package in the last commit.

codecov-io: # [Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2253?src=pr&el=h1) Report
> Merging [#2253](https://codecov.io/gh/aws/aws-sdk-js/pull/2253?src=pr&el=desc) into [master](https://codecov.io/gh/aws/aws-sdk-js/commit/4b433c447b778d2c9efda0cd04c0185ebac46009?src=pr&el=desc) will **decrease** coverage by `84.25%`.
> The diff coverage is `23.07%`.

[![Impacted file tree graph](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/graphs/tree.svg?width=650&token=qXdPGBZWAr&height=150&src=pr)](https://codecov.io/gh/aws/aws-sdk-js/pull/2253?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           master    #2253       +/-   ##
===========================================
- Coverage   96.09%   11.83%   -84.26%     
===========================================
  Files         250       28      -222     
  Lines        7854     1496     -6358     
  Branches     1508      390     -1118     
===========================================
- Hits         7547      177     -7370     
- Misses        307     1319     +1012
```


| [Impacted Files](https://codecov.io/gh/aws/aws-sdk-js/pull/2253?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/core.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL2NvcmUuanM=) | `26.66% <ø> (-73.34%)` | :arrow_down: |
| [lib/model/shape.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL21vZGVsL3NoYXBlLmpz) | `10.04% <0%> (-85.37%)` | :arrow_down: |
| [lib/model/operation.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL21vZGVsL29wZXJhdGlvbi5qcw==) | `10.93% <0%> (-71.03%)` | :arrow_down: |
| [lib/model/api.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL21vZGVsL2FwaS5qcw==) | `22.44% <25%> (-77.56%)` | :arrow_down: |
| [lib/model/collection.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL21vZGVsL2NvbGxlY3Rpb24uanM=) | `33.33% <40%> (-66.67%)` | :arrow_down: |
| [lib/util.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL3V0aWwuanM=) | `0.67% <0%> (-93.68%)` | :arrow_down: |
| [lib/protocol/rest.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL3Byb3RvY29sL3Jlc3QuanM=) | `10.11% <0%> (-89.89%)` | :arrow_down: |
| [lib/event-stream/event-message-chunker-stream.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL2V2ZW50LXN0cmVhbS9ldmVudC1tZXNzYWdlLWNodW5rZXItc3RyZWFtLmpz) | `6.12% <0%> (-89.8%)` | :arrow_down: |
| [lib/protocol/query.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree#diff-bGliL3Byb3RvY29sL3F1ZXJ5Lmpz) | `16% <0%> (-84%)` | :arrow_down: |
| ... and [239 more](https://codecov.io/gh/aws/aws-sdk-js/pull/2253/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2253?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2253?src=pr&el=footer). Last update [4b433c4...0a6c82e](https://codecov.io/gh/aws/aws-sdk-js/pull/2253?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.changes/next-release/feature-Endpoint Discovery-6b8e9130.json b/.changes/next-release/feature-Endpoint Discovery-6b8e9130.json
new file mode 100644
index 000000000..b6b241b53
--- /dev/null
+++ b/.changes/next-release/feature-Endpoint Discovery-6b8e9130.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "Endpoint Discovery",
+  "description": "Some services provide endpoint discovery operations (e.g. 'DescribeEndpoints()'). This SDK feature, if turned on, will automatically request endpoints from services if needed and cache the endpoints returned."
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e080cdb00..1e0693f24 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,6 @@ configuration
 node_modules
 npm-debug.log
 coverage
-vendor
 .yardoc
 doc
 test/browser/build
diff --git a/.npmignore b/.npmignore
index 9f3603451..63f09a5e6 100644
--- a/.npmignore
+++ b/.npmignore
@@ -31,4 +31,3 @@ testem.json
 tasks
 ts
 UPGRADING.md
-vendor
diff --git a/codecov.yml b/codecov.yml
index 6d38452aa..a576b1470 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,3 +1,6 @@
+ignore:
+  - "vendor/"
+
 codecov:
   notify:
     after_n_builds: 4
diff --git a/lib/browser_loader.js b/lib/browser_loader.js
index 48d26479a..7191a3705 100644
--- a/lib/browser_loader.js
+++ b/lib/browser_loader.js
@@ -8,6 +8,8 @@ util.querystring = require('querystring/');
 util.realClock = require('./realclock/browserClock');
 util.environment = 'js';
 util.createEventStream = require('./event-stream/buffered-create-event-stream').createEventStream;
+util.isBrowser = function() { return true; };
+util.isNode = function() { return false; };
 
 var AWS = require('./core');
 
diff --git a/lib/config.d.ts b/lib/config.d.ts
index b0246334d..a563acea8 100644
--- a/lib/config.d.ts
+++ b/lib/config.d.ts
@@ -276,4 +276,15 @@ export abstract class ConfigurationOptions {
      * by DynamoDB.
      */
     dynamoDbCrc32?: boolean;
+    /**
+     * Whether to enable endpoint discovery for operations that allow optionally using an endpoint returned by 
+     * the service.
+     */
+    endpointDiscoveryEnabled?: boolean;
+    /**
+     * The size of the global cache storing endpoints from endpoint
+     * discovery operations. Once endpoint cache is created, updating this setting
+     * cannot change existing cache size.
+     */
+    endpointCacheSize?: number;
 }
diff --git a/lib/config.js b/lib/config.js
index d28e3c6c8..c2d1e3651 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -150,6 +150,17 @@ var PromisesDependency;
  *   @return [Boolean] whether the signature to sign requests with (overriding
  *     the API configuration) is cached. Only applies to the signature version 'v4'.
  *     Defaults to `true`.
+ *
+ * @!attribute endpointDiscoveryEnabled
+ *   @return [Boolean] whether to enable endpoint discovery for operations that
+ *     allow optionally using an endpoint returned by the service.
+ *     Defaults to 'false'
+ *
+ * @!attribute endpointCacheSize
+ *   @return [Number] the size of the global cache storing endpoints from endpoint
+ *     discovery operations. Once endpoint cache is created, updating this setting
+ *     cannot change existing cache size.
+ *     Defaults to 1000
  */
 AWS.Config = AWS.util.inherit({
   /**
@@ -276,6 +287,14 @@ AWS.Config = AWS.util.inherit({
    *   checksum of HTTP response bodies returned by DynamoDB. Default: `true`.
    * @option options clientSideMonitoring [Boolean] whether to collect and
    * publish this client's performance metrics of all its API requests.
+   * @option options endpointDiscoveryEnabled [Boolean] whether to enable endpoint
+   *   discovery for operations that allow optionally using an endpoint returned by
+   *   the service.
+   *   Defaults to 'false'
+   * @option options endpointCacheSize [Number] the size of the global cache storing
+   *   endpoints from endpoint discovery operations. Once endpoint cache is created,
+   *   updating this setting cannot change existing cache size.
+   *   Defaults to 1000
    */
   constructor: function Config(options) {
     if (options === undefined) options = {};
@@ -493,7 +512,9 @@ AWS.Config = AWS.util.inherit({
     signatureCache: true,
     retryDelayOptions: {},
     useAccelerateEndpoint: false,
-    clientSideMonitoring: false
+    clientSideMonitoring: false,
+    endpointDiscoveryEnabled: false,
+    endpointCacheSize: 1000
   },
 
   /**
diff --git a/lib/core.js b/lib/core.js
index f306aef7e..9437af726 100644
--- a/lib/core.js
+++ b/lib/core.js
@@ -68,7 +68,12 @@ AWS.util.update(AWS, {
   /**
    * @api private
    */
-  apiLoader: require('./api_loader')
+  apiLoader: require('./api_loader'),
+
+  /**
+   * @api private
+   */
+  EndpointCache: require('../vendor/endpoint-cache').EndpointCache
 });
 require('./sequential_executor');
 require('./service');
@@ -97,3 +102,8 @@ require('./param_validator');
  *   new AWS.S3().listBuckets(); // prints 'Request took 0.285 seconds'
  */
 AWS.events = new AWS.SequentialExecutor();
+
+//create endpoint cache lazily
+AWS.util.memoizedProperty(AWS, 'endpointCache', function() {
+  return new AWS.EndpointCache(AWS.config.endpointCacheSize);
+}, true);
diff --git a/lib/credentials/shared_ini_file_credentials.js b/lib/credentials/shared_ini_file_credentials.js
index f7a75bbfe..f6244e902 100644
--- a/lib/credentials/shared_ini_file_credentials.js
+++ b/lib/credentials/shared_ini_file_credentials.js
@@ -1,6 +1,6 @@
 var AWS = require('../core');
-var iniLoader = require('../shared-ini').iniLoader;
 var STS = require('../../clients/sts');
+var iniLoader = AWS.util.iniLoader;
 
 /**
  * Represents credentials loaded from shared credentials file
diff --git a/lib/discover_endpoint.js b/lib/discover_endpoint.js
new file mode 100644
index 000000000..ba9f25b2f
--- /dev/null
+++ b/lib/discover_endpoint.js
@@ -0,0 +1,353 @@
+var AWS = require('./core');
+var util = require('./util');
+var endpointDiscoveryEnabledEnv = 'AWS_ENABLE_ENDPOINT_DISCOVERY';
+
+/**
+ * Generate key (except resources and operation part) to index the endpoints in the cache
+ * If input shape has endpointdiscoveryid trait then use
+ *   accessKey + operation + resources + region + service as cache key
+ * If input shape doesn't have endpointdiscoveryid trait then use
+ *   accessKey + region + service as cache key
+ * @return [map<String,String>] object with keys to index endpoints.
+ * @api private
+ */
+function getCacheKey(request) {
+  var service = request.service;
+  var api = service.api || {};
+  var operations = api.operations;
+  var identifiers = {};
+  if (service.config.region) {
+    identifiers.region = service.config.region;
+  }
+  if (api.serviceId) {
+    identifiers.serviceId = api.serviceId
+  }
+  if (service.config.credentials.accessKeyId) {
+    identifiers.accessKeyId = service.config.credentials.accessKeyId
+  }
+  return identifiers;
+}
+
+/**
+ * Recursive helper for marshallCustomIdentifiers().
+ * Looks for required string input members that have 'endpointdiscoveryid' trait.
+ * @api private
+ */
+function marshallCustomIdentifiersHelper(result, params, shape) {
+  if (!shape || params === undefined || params === null) return;
+  if (shape.type === 'structure' && shape.required && shape.required.length > 0) {
+    util.arrayEach(shape.required, function(name) {
+      var memberShape = shape.members[name];
+      if (memberShape.endpointDiscoveryId === true) {
+        var locationName = memberShape.isLocationName ? memberShape.name : name;
+        result[locationName] = String(params[name]);
+      } else {
+        marshallCustomIdentifiersHelper(result, params[name], memberShape);
+      }
+    });
+  }
+}
+
+/**
+ * Get custom identifiers for cache key.
+ * Identifies custom identifiers by checking each shape's `endpointDiscoveryId` trait.
+ * @param [object] request object
+ * @param [object] input shape of the given operation's api
+ * @api private
+ */
+function marshallCustomIdentifiers(request, shape) {
+  var identifiers = {};
+  marshallCustomIdentifiersHelper(identifiers, request.params, shape);
+  return identifiers;
+}
+
+/**
+ * Call endpoint discovery operation when it's optional.
+ * When endpoint is available in cache then use the cached endpoints. If endpoints
+ * are unavailable then use regional endpoints and call endpoint discovery operation
+ * asynchronously. This is turned off by default.
+ * @param [object] request object
+ * @api private
+ */
+function optionalDiscoverEndpoint(request) {
+  var service = request.service;
+  var api = service.api;
+  var operationModel = api.operations ? api.operations[request.operation] : undefined;
+  var inputShape = operationModel ? operationModel.input : undefined;
+
+  var identifiers = marshallCustomIdentifiers(request, inputShape);
+  var cacheKey = getCacheKey(request);
+  if (Object.keys(identifiers).length > 0) {
+    cacheKey = util.update(cacheKey, identifiers);
+    if (operationModel) cacheKey.operation = operationModel.name;
+  }
+  var endpoints = AWS.endpointCache.get(cacheKey);
+  if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
+    //endpoint operation is being made but response not yet received
+    //or endpoint operation just failed in 1 minute
+    return;
+  } else if (endpoints && endpoints.length > 0) {
+    //found endpoint record from cache
+    request.httpRequest.updateEndpoint(endpoints[0].Address);
+  } else {
+    //endpoint record not in cache or outdated. make discovery operation
+    var endpointRequest = service.makeRequest(api.endpointOperation, {
+      Operation: operationModel.name,
+      Identifiers: identifiers,
+    });
+    addApiVersionHeader(endpointRequest);
+    endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
+    endpointRequest.removeListener('retry', AWS.EventListeners.Core.RETRY_CHECK);
+    //put in a placeholder for endpoints already requested, prevent
+    //too much in-flight calls
+    AWS.endpointCache.put(cacheKey, [{
+      Address: '',
+      CachePeriodInMinutes: 1
+    }]);
+    endpointRequest.send(function(err, data) {
+      if (data && data.Endpoints) {
+        AWS.endpointCache.put(cacheKey, data.Endpoints);
+      } else if (err) {
+        AWS.endpointCache.put(cacheKey, [{
+          Address: '',
+          CachePeriodInMinutes: 1 //not to make more endpoint operation in next 1 minute
+        }]);
+      }
+    });
+  }
+}
+
+var requestQueue = {};
+
+/**
+ * Call endpoint discovery operation when it's required.
+ * When endpoint is available in cache then use cached ones. If endpoints are
+ * unavailable then SDK should call endpoint operation then use returned new
+ * endpoint for the api call. SDK will automatically attempt to do endpoint
+ * discovery. This is turned off by default
+ * @param [object] request object
+ * @api private
+ */
+function requiredDiscoverEndpoint(request, done) {
+  var service = request.service;
+  var api = service.api;
+  var operationModel = api.operations ? api.operations[request.operation] : undefined;
+  var inputShape = operationModel ? operationModel.input : undefined;
+
+  var identifiers = marshallCustomIdentifiers(request, inputShape);
+  var cacheKey = getCacheKey(request);
+  if (Object.keys(identifiers).length > 0) {
+    cacheKey = util.update(cacheKey, identifiers);
+    if (operationModel) cacheKey.operation = operationModel.name;
+  }
+  var cacheKeyStr = AWS.EndpointCache.getKeyString(cacheKey);
+  var endpoints = AWS.endpointCache.get(cacheKeyStr); //endpoint cache also accepts string keys
+  if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
+    //endpoint operation is being made but response not yet received
+    //push request object to a pending queue
+    if (!requestQueue[cacheKeyStr]) requestQueue[cacheKeyStr] = [];
+    requestQueue[cacheKeyStr].push({request: request, callback: done});
+    return;
+  } else if (endpoints && endpoints.length > 0) {
+    request.httpRequest.updateEndpoint(endpoints[0].Address);
+    done();
+  } else {
+    var endpointRequest = service.makeRequest(api.endpointOperation, {
+      Operation: operationModel.name,
+      Identifiers: identifiers,
+    });
+    endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
+    addApiVersionHeader(endpointRequest);
+
+    //put in a placeholder for endpoints already requested, prevent
+    //too much in-flight calls
+    AWS.endpointCache.put(cacheKeyStr, [{
+      Address: '',
+      CachePeriodInMinutes: 60 //long-live cache
+    }]);
+    endpointRequest.send(function(err, data) {
+      if (err) {
+        var errorParams = {
+          code: 'EndpointDiscoveryException',
+          message: 'Request cannot be fulfilled without specifying an endpoint',
+          retryable: false
+        };
+        request.response.error = util.error(err, errorParams);
+        AWS.endpointCache.remove(cacheKey);
+
+        //fail all the pending requests in batch
+        if (requestQueue[cacheKeyStr]) {
+          var pendingRequests = requestQueue[cacheKeyStr];
+          util.arrayEach(pendingRequests, function(requestContext) {
+            requestContext.request.response.error = util.error(err, errorParams);
+            requestContext.callback();
+          });
+          delete requestQueue[cacheKeyStr];
+        }
+      } else if (data) {
+        AWS.endpointCache.put(cacheKeyStr, data.Endpoints);
+        request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
+
+        //update the endpoint for all the pending requests in batch
+        if (requestQueue[cacheKeyStr]) {
+          var pendingRequests = requestQueue[cacheKeyStr];
+          util.arrayEach(pendingRequests, function(requestContext) {
+            requestContext.request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
+            requestContext.callback();
+          });
+          delete requestQueue[cacheKeyStr];
+        }
+      }
+      done();
+    });
+  }
+}
+
+/**
+ * add api version header to endpoint operation
+ * @api private
+ */
+function addApiVersionHeader(endpointRequest) {
+  var api = endpointRequest.service.api;
+  var apiVersion = api.apiVersion;
+  if (apiVersion && !endpointRequest.httpRequest.headers['x-amz-api-version']) {
+    endpointRequest.httpRequest.headers['x-amz-api-version'] = apiVersion;
+  }
+}
+
+/**
+ * If api call gets invalid endpoint exception, SDK should attempt to remove the invalid
+ * endpoint from cache.
+ * @api private
+ */
+function invalidateCachedEndpoints(response) {
+  var error = response.error;
+  var httpResponse = response.httpResponse;
+  if (error &&
+    (error.code === 'InvalidEndpointException' || httpResponse.statusCode === 421)
+  ) {
+    var request = response.request;
+    var operations = request.service.api.operations || {};
+    var inputShape = operations[request.operation] ? operations[request.operation].input : undefined;
+    var identifiers = marshallCustomIdentifiers(request, inputShape);
+    var cacheKey = getCacheKey(request);
+    if (Object.keys(identifiers).length > 0) {
+      cacheKey = util.update(cacheKey, identifiers);
+      if (operations[request.operation]) cacheKey.operation = operations[request.operation].name;
+    }
+    AWS.endpointCache.remove(cacheKey);
+  }
+}
+
+/**
+ * If endpoint is explicitly configured, SDK should not do endpoint discovery in anytime.
+ * @param [object] client Service client object.
+ * @api private
+ */
+function hasCustomEndpoint(client) {
+  //if set endpoint is set for specific client, enable endpoint discovery will raise an error.
+  if (client._originalConfig && client._originalConfig.endpoint && client._originalConfig.endpointDiscoveryEnabled === true) {
+    throw util.error(new Error(), {
+      code: 'ConfigurationException',
+      message: 'Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.'
+    });
+  };
+  var svcConfig = AWS.config[client.serviceIdentifier] || {};
+  return Boolean(AWS.config.endpoint || svcConfig.endpoint || (client._originalConfig && client._originalConfig.endpoint));
+}
+
+/**
+ * @api private
+ */
+function isFalsy(value) {
+  return ['false', '0'].indexOf(value) >= 0;
+}
+
+/**
+ * If endpoint discovery should perform for this request when endpoint discovery is optional.
+ * SDK performs config resolution in order like below:
+ * 1. If turned on client configuration(default to off) then turn on endpoint discovery.
+ * 2. If turned on in env AWS_ENABLE_ENDPOINT_DISCOVERY then turn on endpoint discovery.
+ * 3. If turned on in shared ini config file with key 'endpoint_discovery_enabled', then
+ *   turn on endpoint discovery.
+ * @param [object] request request object.
+ * @api private
+ */
+function isEndpointDiscoveryApplicable(request) {
+  var service = request.service || {};
+  if (service.config.endpointDiscoveryEnabled === true) return true;
+  if (Object.prototype.hasOwnProperty.call(process.env, endpointDiscoveryEnabledEnv)) {
+    if (process.env[endpointDiscoveryEnabledEnv] === '' || process.env[endpointDiscoveryEnabledEnv] === undefined) {
+      throw util.error(new Error(), {
+        code: 'ConfigurationException',
+        message: 'environmental variable AWS_ENABLE_ENDPOINT_DISCOVERY cannot be set to nothing'
+      });
+    }
+    if (!isFalsy(process.env[endpointDiscoveryEnabledEnv])) return true;
+  }
+  //shared ini file is only available in Node
+  if (util.isBrowser()) return false;
+
+  var configFile = {};
+  try {
+    configFile = AWS.util.iniLoader ? AWS.util.iniLoader.loadFrom({
+      isConfig: true,
+      filename: process.env[AWS.util.sharedConfigFileEnv]
+    }) : {};
+  } catch (e) {}
+  var sharedFileConfig = configFile[
+    process.env.AWS_PROFILE || AWS.util.defaultProfile
+  ] || {};
+  if (Object.prototype.hasOwnProperty.call(sharedFileConfig, 'endpoint_discovery_enabled')) {
+    if (sharedFileConfig.endpoint_discovery_enabled === undefined) {
+      throw util.error(new Error(), {
+        code: 'ConfigurationException',
+        message: 'config file entry \'endpoint_discovery_enabled\' cannot be set to nothing'
+      });
+    }
+    if (!isFalsy(sharedFileConfig.endpoint_discovery_enabled)) return true;
+  }
+  return false;
+}
+
+/**
+ * attach endpoint discovery logic to request object
+ * @param [object] request
+ * @api private
+ */
+function discoverEndpoint(request, done) {
+  var service = request.service || {};
+  if (hasCustomEndpoint(service) || request.isPresigned()) return done();
+
+  if (!isEndpointDiscoveryApplicable(request)) return done();
+
+  request.httpRequest.appendToUserAgent('endpoint-discovery');
+
+  var operations = service.api.operations || {};
+  var operationModel = operations[request.operation];
+  var isEndpointDiscoveryRequired = operationModel ? operationModel.endpointDiscoveryRequired : 'NULL';
+  switch (isEndpointDiscoveryRequired) {
+    case 'OPTIONAL':
+      optionalDiscoverEndpoint(request);
+      request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
+      done();
+      break;
+    case 'REQUIRED':
+      request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
+      requiredDiscoverEndpoint(request, done);
+      break;
+    case 'NULL':
+    default:
+      done();
+      break;
+  }
+}
+
+module.exports = {
+  discoverEndpoint: discoverEndpoint,
+  requiredDiscoverEndpoint: requiredDiscoverEndpoint,
+  optionalDiscoverEndpoint: optionalDiscoverEndpoint,
+  marshallCustomIdentifiers: marshallCustomIdentifiers,
+  getCacheKey: getCacheKey,
+  invalidateCachedEndpoint: invalidateCachedEndpoints,
+}
\ No newline at end of file
diff --git a/lib/event_listeners.js b/lib/event_listeners.js
index b4b0555ab..d5289599d 100644
--- a/lib/event_listeners.js
+++ b/lib/event_listeners.js
@@ -1,5 +1,6 @@
 var AWS = require('./core');
 var SequentialExecutor = require('./sequential_executor');
+var DISCOVER_ENDPOINT = require('./discover_endpoint').discoverEndpoint;
 /**
  * The namespace used to register global event listeners for request building
  * and sending.
@@ -190,6 +191,9 @@ AWS.EventListeners = {
       }
     });
 
+    var addToHead = true;
+    addAsync('DISCOVER_ENDPOINT', 'sign', DISCOVER_ENDPOINT, addToHead);
+
     addAsync('SIGN', 'sign', function SIGN(req, done) {
       var service = req.service;
       var operations = req.service.api.operations || {};
diff --git a/lib/http.js b/lib/http.js
index 93e6f639d..f6b28aa73 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -151,8 +151,17 @@ AWS.HttpRequest = inherit({
       return AWS.util.queryParamsToString(query);
     }
     return '';
-  }
+  },
 
+  /**
+   * @api private
+   * update httpRequest endpoint with endpoint string
+   */
+  updateEndpoint: function updateEndpoint(endpointStr) {
+    var newEndpoint = new AWS.Endpoint(endpointStr);
+    this.endpoint = newEndpoint;
+    this.path = newEndpoint.path || '/'
+  }
 });
 
 /**
diff --git a/lib/model/api.js b/lib/model/api.js
index 1c35e3a65..822aacd9f 100644
--- a/lib/model/api.js
+++ b/lib/model/api.js
@@ -9,6 +9,7 @@ var property = util.property;
 var memoizedProperty = util.memoizedProperty;
 
 function Api(api, options) {
+  var self = this;
   api = api || {};
   options = options || {};
   options.api = this;
@@ -39,9 +40,15 @@ function Api(api, options) {
     return name;
   });
 
+  function addEndpointOperation(name, operation) {
+    if (operation.endpointoperation === true) {
+      property(self, 'endpointOperation', util.string.lowerFirst(name));
+    }
+  }
+
   property(this, 'operations', new Collection(api.operations, options, function(name, operation) {
     return new Operation(name, operation, options);
-  }, util.string.lowerFirst));
+  }, util.string.lowerFirst, addEndpointOperation));
 
   property(this, 'shapes', new Collection(api.shapes, options, function(name, shape) {
     return Shape.create(shape, options);
diff --git a/lib/model/collection.js b/lib/model/collection.js
index f00fbfac7..ae399a7ab 100644
--- a/lib/model/collection.js
+++ b/lib/model/collection.js
@@ -1,18 +1,19 @@
 var memoizedProperty = require('../util').memoizedProperty;
 
-function memoize(name, value, fn, nameTr) {
+function memoize(name, value, factory, nameTr) {
   memoizedProperty(this, nameTr(name), function() {
-    return fn(name, value);
+    return factory(name, value);
   });
 }
 
-function Collection(iterable, options, fn, nameTr) {
+function Collection(iterable, options, factory, nameTr, callback) {
   nameTr = nameTr || String;
   var self = this;
 
   for (var id in iterable) {
     if (Object.prototype.hasOwnProperty.call(iterable, id)) {
-      memoize.call(self, id, iterable[id], fn, nameTr);
+      memoize.call(self, id, iterable[id], factory, nameTr);
+      if (callback) callback(id, iterable[id]);
     }
   }
 }
diff --git a/lib/model/operation.js b/lib/model/operation.js
index 136b7c4ad..aa72491c1 100644
--- a/lib/model/operation.js
+++ b/lib/model/operation.js
@@ -15,6 +15,13 @@ function Operation(name, operation, options) {
   property(this, 'httpMethod', operation.http.method || 'POST');
   property(this, 'httpPath', operation.http.requestUri || '/');
   property(this, 'authtype', operation.authtype || '');
+  property(
+    this,
+    'endpointDiscoveryRequired',
+    operation.endpointdiscovery ?
+      (operation.endpointdiscovery.required ? 'REQUIRED' : 'OPTIONAL') :
+    'NULL'
+  );
 
   memoizedProperty(this, 'input', function() {
     if (!operation.input) {
diff --git a/lib/model/shape.js b/lib/model/shape.js
index b7088c494..b9b0ccbef 100644
--- a/lib/model/shape.js
+++ b/lib/model/shape.js
@@ -40,6 +40,7 @@ function Shape(shape, options, memberName) {
   property(this, 'isEventPayload', Boolean(shape.eventpayload), false);
   property(this, 'isEventHeader', Boolean(shape.eventheader), false);
   property(this, 'isTimestampFormatSet', Boolean(shape.timestampFormat) || shape.prototype && shape.prototype.isTimestampFormatSet === true, false);
+  property(this, 'endpointDiscoveryId', Boolean(shape.endpointdiscoveryid), false);
 
   if (options.documentation) {
     property(this, 'documentation', shape.documentation);
diff --git a/lib/node_loader.js b/lib/node_loader.js
index 798463b80..2443e6e64 100644
--- a/lib/node_loader.js
+++ b/lib/node_loader.js
@@ -1,5 +1,8 @@
 var util = require('./util');
 
+util.isBrowser = function() { return false; };
+util.isNode = function() { return true; };
+
 // node.js specific modules
 util.crypto.lib = require('crypto');
 util.Buffer = require('buffer').Buffer;
@@ -14,7 +17,8 @@ util.realClock = require('./realclock/nodeClock');
 util.clientSideMonitoring = {
   Publisher: require('./publisher').Publisher,
   configProvider: require('./publisher/configuration'),
-}
+};
+util.iniLoader = require('./shared-ini').iniLoader;
 
 var AWS;
 
@@ -89,7 +93,7 @@ AWS.util.update(AWS.Config.prototype.keys, {
         {filename: env[AWS.util.sharedCredentialsFileEnv]},
         {isConfig: true, filename: env[AWS.util.sharedConfigFileEnv]}
       ];
-      var iniLoader = require('./shared-ini').iniLoader;
+      var iniLoader = AWS.util.iniLoader;
       while (!region && toCheck.length) {
         var configFile = iniLoader.loadFrom(toCheck.shift());
         var profile = configFile[env.AWS_PROFILE || AWS.util.defaultProfile];
diff --git a/lib/publisher/configuration.js b/lib/publisher/configuration.js
index 4948679ac..273e7ea75 100644
--- a/lib/publisher/configuration.js
+++ b/lib/publisher/configuration.js
@@ -1,5 +1,5 @@
 var AWS = require('../core');
-var iniLoader = require('../shared-ini').iniLoader;
+
 /**
  * Resolve client-side monitoring configuration from either environmental variables
  * or shared config file. Configurations from environmental variables have higher priority
@@ -40,7 +40,7 @@ function fromEnvironment(config) {
 function fromConfigFile(config) {
   var sharedFileConfig;
   try {
-    var configFile = iniLoader.loadFrom({
+    var configFile = AWS.util.iniLoader.loadFrom({
       isConfig: true,
       filename: process.env[AWS.util.sharedConfigFileEnv]
     });
diff --git a/lib/react-native-loader.js b/lib/react-native-loader.js
index f8eb46da0..ce37c8864 100644
--- a/lib/react-native-loader.js
+++ b/lib/react-native-loader.js
@@ -1,4 +1,8 @@
 var AWS = require('./core');
+
+AWS.util.isBrowser = function() { return true; };
+AWS.util.isNode = function() { return false; };
+
 // react-native specific modules
 AWS.util.crypto.lib = require('./browserCryptoLib');
 AWS.util.Buffer = require('buffer/').Buffer;
diff --git a/lib/sequential_executor.js b/lib/sequential_executor.js
index 3342022b7..e40bc59f5 100644
--- a/lib/sequential_executor.js
+++ b/lib/sequential_executor.js
@@ -181,9 +181,9 @@ AWS.SequentialExecutor = AWS.util.inherit({
   /**
    * @api private
    */
-  addNamedAsyncListener: function addNamedAsyncListener(name, eventName, callback) {
+  addNamedAsyncListener: function addNamedAsyncListener(name, eventName, callback, toHead) {
     callback._isAsync = true;
-    return this.addNamedListener(name, eventName, callback);
+    return this.addNamedListener(name, eventName, callback, toHead);
   },
 
   /**
@@ -232,4 +232,4 @@ AWS.SequentialExecutor.prototype.addListener = AWS.SequentialExecutor.prototype.
 /**
  * @api private
  */
-module.exports = AWS.SequentialExecutor;
+module.exports = AWS.SequentialExecutor;
\ No newline at end of file
diff --git a/lib/util.js b/lib/util.js
index 4f51413e2..e1f9ac047 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -36,8 +36,6 @@ var util = {
     return agent;
   },
 
-  isBrowser: function isBrowser() { return process && process.browser; },
-  isNode: function isNode() { return !util.isBrowser(); },
   uriEscape: function uriEscape(string) {
     var output = encodeURIComponent(string);
     output = output.replace(/[^A-Za-z0-9_.~\-%]+/g, escape);
diff --git a/package.json b/package.json
index beae5a656..87f3f316c 100644
--- a/package.json
+++ b/package.json
@@ -143,4 +143,4 @@
     "helper-test": "mocha scripts/lib/test-helper.spec.js",
     "csm-functional-test": "mocha test/publisher/functional_test"
   }
-}
\ No newline at end of file
+}
diff --git a/scripts/region-checker/whitelist.js b/scripts/region-checker/whitelist.js
index 66ee732b4..50172a2f3 100644
--- a/scripts/region-checker/whitelist.js
+++ b/scripts/region-checker/whitelist.js
@@ -2,7 +2,7 @@ var whitelist = {
     '/config.js': [
         24,
         25,
-        165
+        176
     ],
     '/credentials/cognito_identity_credentials.js': [
         78,
diff --git a/vendor/endpoint-cache/index.d.ts b/vendor/endpoint-cache/index.d.ts
new file mode 100644
index 000000000..d324395a6
--- /dev/null
+++ b/vendor/endpoint-cache/index.d.ts
@@ -0,0 +1,27 @@
+/**
+ * Output shape for endpoint discovery operations
+ */
+export type DiscoveredEndpoints = Array<{Address?: string, CachePeriodInMinutes?: number}>
+declare type EndpointRecords = Array<{
+  Address: string;
+  Expire: number;
+}>;
+export interface EndpointIdentifier {
+    [key: string]: string | undefined;
+    serviceId?: string;
+    region?: string;
+    accessKeyId?: string;
+    operation?: string;
+}
+export declare class EndpointCache {
+    readonly maxSize: number;
+    private cache;
+    constructor(maxSize?: number);
+    readonly size: number;
+    put(key: EndpointIdentifier | string, value: DiscoveredEndpoints): void;
+    get(key: EndpointIdentifier | string): EndpointRecords | undefined;
+    static getKeyString(key: EndpointIdentifier): string;
+    private populateValue;
+    empty(): void;
+    remove(key: EndpointIdentifier | string): void;
+}
diff --git a/vendor/endpoint-cache/index.js b/vendor/endpoint-cache/index.js
new file mode 100644
index 000000000..180911142
--- /dev/null
+++ b/vendor/endpoint-cache/index.js
@@ -0,0 +1,69 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var LRU_1 = require("./utils/LRU");
+var CACHE_SIZE = 1000;
+/**
+ * Inspired node-lru-cache[https://github.com/isaacs/node-lru-cache]
+ */
+var EndpointCache = /** @class */ (function () {
+    function EndpointCache(maxSize) {
+        if (maxSize === void 0) { maxSize = CACHE_SIZE; }
+        this.maxSize = maxSize;
+        this.cache = new LRU_1.LRUCache(maxSize);
+    }
+    ;
+    Object.defineProperty(EndpointCache.prototype, "size", {
+        get: function () {
+            return this.cache.length;
+        },
+        enumerable: true,
+        configurable: true
+    });
+    EndpointCache.prototype.put = function (key, value) {
+      var keyString = typeof key !== 'string' ? EndpointCache.getKeyString(key) : key;
+        var endpointRecord = this.populateValue(value);
+        this.cache.put(keyString, endpointRecord);
+    };
+    EndpointCache.prototype.get = function (key) {
+      var keyString = typeof key !== 'string' ? EndpointCache.getKeyString(key) : key;
+        var now = Date.now();
+        var records = this.cache.get(keyString);
+        if (records) {
+            for (var i = 0; i < records.length; i++) {
+                var record = records[i];
+                if (record.Expire < now) {
+                    this.cache.remove(keyString);
+                    return undefined;
+                }
+            }
+        }
+        return records;
+    };
+    EndpointCache.getKeyString = function (key) {
+        var identifiers = [];
+        var identifierNames = Object.keys(key).sort();
+        for (var i = 0; i < identifierNames.length; i++) {
+            var identifierName = identifierNames[i];
+            if (key[identifierName] === undefined)
+                continue;
+            identifiers.push(key[identifierName]);
+        }
+        return identifiers.join(' ');
+    };
+    EndpointCache.prototype.populateValue = function (endpoints) {
+        var now = Date.now();
+        return endpoints.map(function (endpoint) { return ({
+            Address: endpoint.Address || '',
+            Expire: now + (endpoint.CachePeriodInMinutes || 1) * 60 * 1000
+        }); });
+    };
+    EndpointCache.prototype.empty = function () {
+        this.cache.empty();
+    };
+    EndpointCache.prototype.remove = function (key) {
+      var keyString = typeof key !== 'string' ? EndpointCache.getKeyString(key) : key;
+        this.cache.remove(keyString);
+    };
+    return EndpointCache;
+}());
+exports.EndpointCache = EndpointCache;
\ No newline at end of file
diff --git a/vendor/endpoint-cache/utils/LRU.d.ts b/vendor/endpoint-cache/utils/LRU.d.ts
new file mode 100644
index 000000000..5a1fc8e4d
--- /dev/null
+++ b/vendor/endpoint-cache/utils/LRU.d.ts
@@ -0,0 +1,16 @@
+export declare class LRUCache<T> {
+    private headerNode;
+    private tailNode;
+    private nodeMap;
+    private size;
+    private readonly sizeLimit;
+    constructor(size: number);
+    readonly length: number;
+    private prependToList;
+    private removeFromTail;
+    private detachFromList;
+    get(key: string): T | undefined;
+    remove(key: string): void;
+    put(key: string, value: T): void;
+    empty(): void;
+}
diff --git a/vendor/endpoint-cache/utils/LRU.js b/vendor/endpoint-cache/utils/LRU.js
new file mode 100644
index 000000000..5b39d2647
--- /dev/null
+++ b/vendor/endpoint-cache/utils/LRU.js
@@ -0,0 +1,107 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var LinkedListNode = /** @class */ (function () {
+    function LinkedListNode(key, value) {
+        this.key = key;
+        this.value = value;
+    }
+    return LinkedListNode;
+}());
+var LRUCache = /** @class */ (function () {
+    function LRUCache(size) {
+        this.nodeMap = {};
+        this.size = 0;
+        if (typeof size !== 'number' || size < 1) {
+            throw new Error('Cache size can only be positive number');
+        }
+        this.sizeLimit = size;
+    }
+    Object.defineProperty(LRUCache.prototype, "length", {
+        get: function () {
+            return this.size;
+        },
+        enumerable: true,
+        configurable: true
+    });
+    LRUCache.prototype.prependToList = function (node) {
+        if (!this.headerNode) {
+            this.tailNode = node;
+        }
+        else {
+            this.headerNode.prev = node;
+            node.next = this.headerNode;
+        }
+        this.headerNode = node;
+        this.size++;
+    };
+    LRUCache.prototype.removeFromTail = function () {
+        if (!this.tailNode) {
+            return undefined;
+        }
+        var node = this.tailNode;
+        var prevNode = node.prev;
+        if (prevNode) {
+            prevNode.next = undefined;
+        }
+        node.prev = undefined;
+        this.tailNode = prevNode;
+        this.size--;
+        return node;
+    };
+    LRUCache.prototype.detachFromList = function (node) {
+        if (this.headerNode === node) {
+            this.headerNode = node.next;
+        }
+        if (this.tailNode === node) {
+            this.tailNode = node.prev;
+        }
+        if (node.prev) {
+            node.prev.next = node.next;
+        }
+        if (node.next) {
+            node.next.prev = node.prev;
+        }
+        node.next = undefined;
+        node.prev = undefined;
+        this.size--;
+    };
+    LRUCache.prototype.get = function (key) {
+        if (this.nodeMap[key]) {
+            var node = this.nodeMap[key];
+            this.detachFromList(node);
+            this.prependToList(node);
+            return node.value;
+        }
+    };
+    LRUCache.prototype.remove = function (key) {
+        if (this.nodeMap[key]) {
+            var node = this.nodeMap[key];
+            this.detachFromList(node);
+            delete this.nodeMap[key];
+        }
+    };
+    LRUCache.prototype.put = function (key, value) {
+        if (this.nodeMap[key]) {
+            this.remove(key);
+        }
+        else if (this.size === this.sizeLimit) {
+            var tailNode = this.removeFromTail();
+            var key_1 = tailNode.key;
+            delete this.nodeMap[key_1];
+        }
+        var newNode = new LinkedListNode(key, value);
+        this.nodeMap[key] = newNode;
+        this.prependToList(newNode);
+    };
+    LRUCache.prototype.empty = function () {
+        var keys = Object.keys(this.nodeMap);
+        for (var i = 0; i < keys.length; i++) {
+            var key = keys[i];
+            var node = this.nodeMap[key];
+            this.detachFromList(node);
+            delete this.nodeMap[key];
+        }
+    };
+    return LRUCache;
+}());
+exports.LRUCache = LRUCache;
\ No newline at end of file


Test patch:
diff --git a/test/config.spec.js b/test/config.spec.js
index 8a4be2a2d..084de261f 100644
--- a/test/config.spec.js
+++ b/test/config.spec.js
@@ -1,7 +1,7 @@
 var hasProp = {}.hasOwnProperty;
 var helpers = require('./helpers');
 var AWS = helpers.AWS;
-var iniLoader = require('../lib/shared-ini').iniLoader;
+var iniLoader = AWS.util.iniLoader;
 var configure = function(options) {
   return new AWS.Config(options);
 };
@@ -41,7 +41,7 @@ describe('AWS.Config', function() {
 
     afterEach(function() {
       process.env = oldEnv;
-      iniLoader.clearCachedFiles();
+      if (iniLoader) iniLoader.clearCachedFiles();//iniLoader not available in browsers
     });
 
     it('defaults to undefined', function() {
diff --git a/test/credentials.spec.js b/test/credentials.spec.js
index e28452c9c..7978ae172 100644
--- a/test/credentials.spec.js
+++ b/test/credentials.spec.js
@@ -8,7 +8,7 @@
 
   STS = require('../clients/sts');
 
-  var iniLoader = require('../lib/shared-ini').iniLoader;
+  var iniLoader = AWS.util.iniLoader;
 
   validateCredentials = function(creds, key, secret, session) {
     expect(creds.accessKeyId).to.equal(key || 'akid');
diff --git a/test/discover_endpoint.spec.js b/test/discover_endpoint.spec.js
new file mode 100644
index 000000000..fac646707
--- /dev/null
+++ b/test/discover_endpoint.spec.js
@@ -0,0 +1,841 @@
+var helpers = require('./helpers');
+var http = require('http');
+var url = require('url')
+var AWS = helpers.AWS
+var endpoint_discovery_module = require('../lib/discover_endpoint');
+var iniLoader = AWS.util.iniLoader;
+var getCacheKey = endpoint_discovery_module.getCacheKey;
+var marshallCustomIdentifiers = endpoint_discovery_module.marshallCustomIdentifiers;
+
+
+var api = {
+  metadata: {
+    apiVersion: '2018-09-19',
+    endpointPrefix: 'mockservice',
+    serviceId: 'MockService',
+    protocol: 'json'
+  },
+  operations: {
+    DescribeEndpoints: {
+      name: 'DescribeEndpoints',
+      http: {},
+      endpointoperation: true,
+      input: {
+        type: 'structure',
+        required: ['Operation'],
+        members: {
+          Operation: {},
+          Identifiers: {
+            type: 'map',
+            key: {},
+            value: {},
+          }
+        }
+      },
+      output: {
+        type: 'structure',
+        required: ['Endpoints'],
+        members: {
+          Endpoints: {
+            type: 'list',
+            member: {
+              type: 'structure',
+              members: {
+                Address: {},
+                CachePeriodInMinutes: {type: 'integer'}
+              }
+            }
+          }
+        }
+      }
+    },
+    OptionalEDOperation: {
+      name: 'OptionalEDOperation',
+      http: {},
+      endpointdiscovery: {},
+      input: {
+        type: 'structure',
+        members: {
+          Query: {}
+        }
+      },
+      output: {
+        type: 'structure',
+        members: {
+          Item: {}
+        }
+      }
+    },
+    RequiredEDOperation: {
+      name: 'RequiredEDOperation',
+      http: {},
+      endpointdiscovery: {
+        required: true
+      },
+      input: {
+        type: 'structure',
+        required: ['Query'],
+        members: {
+          Query: {
+            type: 'string',
+            endpointdiscoveryid: true
+          },
+          Record: {
+            type: 'string',
+          }
+        }
+      },
+      output: {
+        type: 'structure',
+        members: {
+          Item: {}
+        }
+      }
+    },
+    ComplexEDOperation: {
+      name: 'ComplexEDOperation',
+      http: {},
+      endpointdiscovery: {
+        required: false
+      },
+      input: {
+        type: 'structure',
+        required: ['Query'],
+        members: {
+          Query: {
+            type: 'structure',
+            required: ['Record'],
+            members: {
+              Record: {
+                type: 'string',
+                locationName: 'RecordItem',
+                endpointdiscoveryid: true
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+describe('endpoint discovery', function() {
+  afterEach(function() {
+    AWS.config.endpoint = undefined;
+    if (iniLoader) iniLoader.clearCachedFiles(); //iniLoader not available in browsers
+  });
+
+  describe('getCacheKey', function() {
+    it ('generate correct keys', function() {
+      var client = new AWS.Service({
+        region: 'fake-region-1',
+        apiConfig: new AWS.Model.Api(api)
+      });
+      var request = client.makeRequest('optionalEDOperation', {});
+      var cacheKey = getCacheKey(request);
+      expect(cacheKey).to.eql({
+        region: 'fake-region-1',
+        serviceId: 'MockService',
+        accessKeyId: 'akid'
+      })
+    });
+  
+    it('generate correct keys with endpointdiscoveryid traits', function() {
+      var spy = helpers.spyOn(AWS.endpointCache, 'get').andReturn([{
+        Address: 'https://fakeService.amaoznaws.com'
+      }]);
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        region: 'fake-region-1',
+        apiConfig: new AWS.Model.Api(api)
+      });
+      helpers.mockHttpResponse(200, {}, '{}');
+      var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+      request.send();
+      expect(spy.calls.length).to.eql(1);
+      expect(spy.calls[0].arguments[0]).to.eql(AWS.EndpointCache.getKeyString({
+        region: 'fake-region-1',
+        operation: 'RequiredEDOperation',
+        serviceId: 'MockService',
+        accessKeyId: 'akid',
+        Query: 'query'
+      }));
+    });
+  });
+  
+  describe('marshallCustomIdentifiers', function() {
+    it('can marshall params according to endpointdiscoveryid trait', function() {
+      var MockService = helpers.MockServiceFromApi(api);
+      var client = new MockService();
+      var ids = marshallCustomIdentifiers({
+        params: {Query: 'query', Record: 'record'}
+      }, client.api.operations.requiredEDOperation.input);
+      expect(ids).to.eql({Query: 'query'});
+      ids = marshallCustomIdentifiers({
+        params: {Query: {Record: 'record'}}
+      }, client.api.operations.complexEDOperation.input);
+      expect(ids).to.eql({RecordItem: 'record'});
+    });
+  });
+  
+  describe('optionalDiscoverEndpoint', function() {
+    beforeEach(function(){
+      AWS.endpointCache.empty();
+    })
+    
+    it('gets first corresponding endpoint from endpoint cache', function() {
+      var spy = helpers.spyOn(AWS.endpointCache, 'get').andReturn([{
+        Address: 'https://fakeService.amazonaws.com:22/path'
+      }, {
+        Address: 'https://fakeService.amaoznaws.com'
+      }]);
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        region: 'fake-region-1',
+        apiConfig: new AWS.Model.Api(api)
+      });
+      helpers.mockHttpResponse(200, {}, '{}')
+      var request = client.makeRequest('optionalEDOperation', {Query: 'query'});
+      request.send();
+      expect(spy.calls.length).to.eql(1);
+      expect(request.httpRequest.endpoint).to.include({
+        hostname: 'fakeservice.amazonaws.com',
+        port: 22,
+        protocol: 'https:',
+        path: '/path'
+      })
+    });
+  
+    it('use regional endpoint of new endpoint cannot be discovered', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        region: 'fake-region-1',
+        apiConfig: new AWS.Model.Api(api)
+      });
+      var request = client.makeRequest('optionalEDOperation', {Query: 'query'});
+      expect(request.httpRequest.endpoint.hostname).to.eql('mockservice.fake-region-1.amazonaws.com')
+    });
+  
+    it('put in endpoint cache is endpoint operation succeeds', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        region: 'fake-region-1',
+        maxRetries: 0,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      var spy = helpers.spyOn(AWS.endpointCache, 'put').andCallThrough();
+      var request = client.makeRequest('optionalEDOperation', {Query: 'query'});
+      expect(request.httpRequest.endpoint.hostname).to.eql('mockservice.fake-region-1.amazonaws.com');
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      request.send();
+      expect(spy.calls.length).to.eql(2);
+      expect(spy.calls[0].arguments[0]).to.eql({
+        accessKeyId: 'akid',
+        region: 'fake-region-1',
+        serviceId: 'MockService'
+      });
+      expect(spy.calls[0].arguments[1]).to.eql([{Address: '', CachePeriodInMinutes: 1}]);
+      expect(spy.calls[1].arguments[0]).to.eql({
+        accessKeyId: 'akid',
+        region: 'fake-region-1',
+        serviceId: 'MockService'
+      });
+      expect(spy.calls[1].arguments[1]).to.eql([{Address: 'https://cell1.fakeservice.amazonaws.com/fakeregion', CachePeriodInMinutes: 1}]);
+      var makeRequestSpy = helpers.spyOn(client, 'makeRequest').andCallThrough();
+      var replicatedRequest = client.makeRequest('optionalEDOperation', {Query: 'query'});
+      replicatedRequest.send(function() {
+        expect(this.request.httpRequest.endpoint).to.include({
+          hostname: 'cell1.fakeservice.amazonaws.com',
+          protocol: 'https:',
+          path: '/fakeregion'
+        });
+        //do not request endpoint again
+        expect(makeRequestSpy.calls.length).to.eql(1);
+        expect(makeRequestSpy.calls[0].arguments[0]).to.eql('optionalEDOperation');
+      });
+    });
+  
+    it('make one endpoint operation for same cache keys before endpoint operation response returning', function() {
+      var spy = helpers.spyOn(AWS.HttpClient, 'getInstance').andCallThrough();
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        region: 'fake-region-1',
+        maxRetries: 0,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      //should not make 4 calls which request endpoint once for every api call
+      expect(spy.calls.length).to.eql(3)
+    });
+  
+    it('should put in an undefined cache record valid for 1 minute when failed', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        maxRetries: 0,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      helpers.mockHttpResponse(400, {}, '');
+      var spy = helpers.spyOn(AWS.endpointCache, 'put').andCallThrough();
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      expect(spy.calls.length).to.eql(2);
+      expect(spy.calls[1].arguments[0]).to.eql({
+        accessKeyId: 'akid',
+        region: 'mock-region',
+        serviceId: 'MockService'
+      });
+      expect(spy.calls[1].arguments[1]).to.eql([{
+        Address: '',
+        CachePeriodInMinutes: 1
+      }])
+    });
+
+    it('evict invalid endpoint records from cache if InvalidEndpointException encountered', function() {
+      var client = new AWS.Service({
+        region: 'fake-region-1',
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      helpers.mockHttpResponse(400, {}, '{"__type":"InvalidEndpointException", "Message":"Error Message" }');
+      var cacheKey = {
+        accessKeyId: 'akid',
+        region: 'fake-region-1',
+        serviceId: 'MockService'
+      }
+      AWS.endpointCache.put(cacheKey, [{Address: 'https://cell1.fakeservice.amazonaws.com/fakeregion'}]);
+      var cacheRemoveSpy = helpers.spyOn(AWS.endpointCache, 'remove').andCallThrough();
+      var request = client.makeRequest('optionalEDOperation', {Query: 'query'});
+      request.send();
+      expect(request.httpRequest.endpoint.href).to.eql('https://cell1.fakeservice.amazonaws.com/fakeregion');
+      expect(cacheRemoveSpy.calls.length).to.eql(1);
+      expect(cacheRemoveSpy.calls[0].arguments[0]).to.eql(cacheKey);
+      expect(AWS.endpointCache.size).to.eql(0);
+    });
+
+
+    it('add "x-amz-api-version" header to discovery endpoint operation in optional endpoint discovery', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api),
+      });
+      var https = require('https');
+      helpers.spyOn(https, 'request').andCallThrough();
+      var request = client.makeRequest('optionalEDOperation', {Query: 'query'});
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      var spy = helpers.createSpy('send inject');
+      AWS.events.on('sign', function(req) {
+        if(req.operation === 'describeEndpoints') {
+          spy(req)
+        }
+      })
+      request.send();
+      expect(spy.calls.length).to.eql(1);
+      expect(spy.calls[0].arguments[0].httpRequest.headers['x-amz-api-version']).to.eql('2018-09-19');
+    });
+  });
+  
+  describe('requiredDiscoveryEndpoint', function() {
+    beforeEach(function(){
+      AWS.endpointCache.empty()
+    });
+  
+    it('fail when endpoint discovery fails', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        maxRetries: 0,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      helpers.mockHttpResponse(400, {}, '');
+      var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+      request.send(function(err, data) {
+        expect(err).not.to.eql(null);
+        expect(data).to.eql(null);
+        expect(err.code).to.eql('EndpointDiscoveryException');
+        expect(err.message).to.eql('Request cannot be fulfilled without specifying an endpoint');
+      })
+    });
+  
+    it('endpoint operation will be called at the same rate of api requests', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      helpers.mockHttpResponse(400, {}, '');
+      var makeRequestSpy = helpers.spyOn(client, 'makeRequest').andCallThrough();
+      for (var apiRequestCount = 0; apiRequestCount < 10; apiRequestCount++) {
+        var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+        request.send();
+      }
+      expect(makeRequestSpy.calls.length).to.eql(20);
+      var apiRequestCount = 0;
+      var endpointOperationRequestCount = 0;
+      for (var i = 0; i < makeRequestSpy.calls.length; i++) {
+        var operationName = makeRequestSpy.calls[i].arguments[0];
+        if (operationName === 'requiredEDOperation') apiRequestCount++;
+        else if (operationName === 'describeEndpoints') endpointOperationRequestCount++;
+      }
+      expect(apiRequestCount).to.eql(10);
+      expect(endpointOperationRequestCount).to.eql(10);
+    });
+  
+    it('put in endpoint cache and use returned endpoint when endpoint discovery succeeds', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+      expect(request.httpRequest.endpoint.hostname).to.eql('mockservice.mock-region.amazonaws.com');
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell2.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      var putCacheCount = 0;
+      var putInCacheSpy = helpers.spyOn(AWS.endpointCache, 'put').andCallFake(function() {
+        //when discover endpoint optionally, \'put\' method is called twice each time
+        if (arguments[1].length > 0 && arguments[1][0].Address !== '') putCacheCount++;
+        //call through origin put method
+        putInCacheSpy.origMethod.apply(putInCacheSpy.object, arguments);
+      });
+      request.send();
+      expect(putInCacheSpy.calls.length).to.eql(2);
+      expect(putCacheCount).to.eql(1);
+      expect(putInCacheSpy.calls[1].arguments[1]).to.eql([{Address: 'https://cell2.fakeservice.amazonaws.com/fakeregion', CachePeriodInMinutes: 1}]);
+      var makeRequestSpy = helpers.spyOn(client, 'makeRequest').andCallThrough();
+      var replicatedRequest = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+      replicatedRequest.send(function() {
+        expect(this.request.httpRequest.endpoint).to.include({
+          hostname: 'cell2.fakeservice.amazonaws.com',
+          protocol: 'https:',
+          path: '/fakeregion'
+        });
+        //do not request endpoint again
+        expect(makeRequestSpy.calls.length).to.eql(1);
+        expect(makeRequestSpy.calls[0].arguments[0]).to.eql('requiredEDOperation');
+      });
+    });
+
+    it('evict invalid endpoint records from cache if InvalidEndpointException encountered', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      helpers.mockHttpResponse(400, {}, '{"__type":"InvalidEndpointException", "Message":"Error Message" }');
+      var cacheKey = {
+        accessKeyId: 'akid',
+        operation: 'RequiredEDOperation',
+        region: 'mock-region',
+        serviceId: 'MockService',
+        Query: 'query'
+      }
+      AWS.endpointCache.put(cacheKey, [{Address: 'https://cell1.fakeservice.amazonaws.com/fakeregion', CachePeriodInMinutes: 1}]);
+      var cacheRemoveSpy = helpers.spyOn(AWS.endpointCache, 'remove').andCallThrough();
+      var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+      request.send();
+      expect(request.httpRequest.endpoint.href).to.eql('https://cell1.fakeservice.amazonaws.com/fakeregion');
+      expect(cacheRemoveSpy.calls.length).to.eql(1);
+      expect(cacheRemoveSpy.calls[0].arguments[0]).to.eql(cacheKey);
+      expect(AWS.endpointCache.size).to.eql(0);
+    });
+
+    it('add "x-amz-api-version" header to discovery endpoint operation in required endpoint discovery', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api),
+      });
+      var https = require('https');
+      helpers.spyOn(https, 'request').andCallThrough();
+      var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell2.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      var spy = helpers.createSpy('send inject');
+      AWS.events.on('sign', function(req) {
+        if(req.operation === 'describeEndpoints') {
+          spy(req)
+        }
+      });
+      request.send();
+      expect(spy.calls.length).to.eql(1);
+      expect(spy.calls[0].arguments[0].httpRequest.headers['x-amz-api-version']).to.eql('2018-09-19');
+    });
+
+    if (AWS.util.isNode()) {
+      describe('not make more endpoint operation requests if there is an in-flight request', function() {
+        var port;
+        var client;
+        var app;
+
+        function extractOperation(request) {
+          if (request.headers['x-amz-target']) {
+            var target = request.headers['x-amz-target'];
+            var targetArr = target.split('.');
+            return targetArr[targetArr.length - 1];
+          }
+        }
+
+        var server = http.createServer(function(req, res) {
+          app(req, res);
+        });
+
+        beforeEach(function() {
+          port = 1024 + parseInt(Math.random() * 1024);
+          server.listen(port);
+
+          AWS.events.on('build', function(req) {
+            req.httpRequest.updateEndpoint('http://127.0.0.1:' + port);
+          });
+    
+          client = new AWS.Service({
+            endpointDiscoveryEnabled: true,
+            apiConfig: new AWS.Model.Api(api),
+          });
+        });
+
+        afterEach(function() {
+          AWS.events.removeAllListeners();
+          try {server.close()} catch(e) {}//fix service node running error in node 0.10
+        });
+
+        it('endpoint operation succeed', function(done) {
+          var concurrency = 10;
+          var requestsCounter = 0;
+          var endpointOperationCount = 0;
+          var apiOperationCount = 0;
+          app = function(req, res) {
+            var timer;
+            var operation = extractOperation(req);
+            if (operation) {
+              res.writeHead(200, {
+                'Content-Type': 'text/plain'
+              });
+              if (operation === 'DescribeEndpoints') {
+                endpointOperationCount++;
+                timer = setTimeout(function() {
+                  res.write('{"Endpoints": [{"Address": "http://127.0.0.1:' + port + '/discovered", "CachePeriodInMinutes": 10}]}');
+                  res.end();
+                }, 50);
+              } else {
+                apiOperationCount++;
+                expect(url.parse(req.url).path).to.eql('/discovered');
+                res.end('{"Operation": "' + operation + '"}');
+              }
+            } else {
+              res.writeHead(404, {});
+              res.end();
+            }
+            if (timer) {
+              clearTimeout(timer);
+              timer = null;
+            }
+          }
+          for (var i = 0; i < concurrency; i++) {
+            var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+            request.send(function(err, data) {
+              requestsCounter ++;
+              if (requestsCounter === concurrency) {
+                AWS.events.removeAllListeners();
+                server.close();
+                expect(endpointOperationCount).to.eql(1);
+                expect(apiOperationCount).to.eql(concurrency);
+                done();
+              }
+            });
+          }
+        });
+
+        it('endpoint operation fail', function(done) {
+          var concurrency = 10;
+          var requestsCounter = 0;
+          var endpointOperationCount = 0;
+          var apiOperationCount = 0;
+          app = function(req, res) {
+            var timer;
+            var operation = extractOperation(req);
+            if (operation) {
+              if (operation === 'DescribeEndpoints') {
+                endpointOperationCount++;
+                timer = setTimeout(function() {
+                  res.statusCode = 400;
+                  res.write('{"__type": "endpoint is currently unavailable"}');
+                  res.end();
+                }, 50);
+              } else {
+                apiOperationCount++;
+                res.statusCode = 200;
+                res.end('{"Operation": "' + operation + '"}');
+              }
+            } else {
+              res.writeHead(404, {});
+              res.end();
+            }
+            if (timer) {
+              clearTimeout(timer);
+              timer = null;
+            }
+          }
+          var failedRequestCounter = 0;
+          for (var i = 0; i < concurrency; i++) {
+            var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+            request.send(function(err, data) {
+              requestsCounter ++;
+              if (err) failedRequestCounter ++;
+              expect(err.code).to.eql('EndpointDiscoveryException');
+              expect(err.message).to.eql('Request cannot be fulfilled without specifying an endpoint');
+              if (requestsCounter === concurrency) {
+                AWS.events.removeAllListeners();
+                server.close();
+                expect(endpointOperationCount).to.eql(1);
+                expect(apiOperationCount).to.eql(0);
+                expect(requestsCounter).to.eql(concurrency);
+                expect(failedRequestCounter).to.eql(concurrency);
+                done();
+              }
+            });
+          }
+        });
+      });
+    }
+  });
+  
+  describe('discoverEndpoints', function() {
+    beforeEach(function(){
+      AWS.endpointCache.empty();
+    });
+  
+    it('accesses a single global endpoint cache for different services', function() {
+      var clientOptions = {
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api)
+      };
+      var client1 = new AWS.Service(clientOptions);
+      var client2 = new AWS.Service(clientOptions);
+      var putCacheCount = 0;
+      var putInCacheSpy = helpers.spyOn(AWS.endpointCache, 'put').andCallFake(function() {
+        //when discover endpoint optionally, \'put\' method is called twice each time
+        if (arguments[1].length > 0 && arguments[1][0].Address !== '') putCacheCount++;
+        //call through origin put method
+        putInCacheSpy.origMethod.apply(putInCacheSpy.object, arguments);
+      });
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      client1.makeRequest('optionalEDOperation', {}).send();
+      expect(putCacheCount).to.eql(1);
+      expect(putInCacheSpy.calls.length).to.eql(2);
+      client1.makeRequest('requiredEDOperation', {}).send();
+      expect(putCacheCount).to.eql(2);
+      //client2 will use the same cache so it won't request new endpoints
+      client2.makeRequest('optionalEDOperation', {}).send();
+      expect(putCacheCount).to.eql(2);
+      client2.makeRequest('requiredEDOperation', {}).send();
+      expect(putCacheCount).to.eql(2);
+    });
+  
+    it('default not run endpoint operation for optional endpoint discovery', function() {
+      var client = new AWS.Service({
+        apiConfig: new AWS.Model.Api(api)
+      });
+      var putInCacheSpy = helpers.spyOn(AWS.endpointCache, 'put').andCallThrough();
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      expect(putInCacheSpy.calls.length).to.eql(0);
+      client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api)
+      });
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      expect(putInCacheSpy.calls.length).to.eql(1);
+    });
+  
+    it('if endpoint specified for client, custom endpoint should be preferred', function() {
+      var client = new AWS.Service({
+        apiConfig: new AWS.Model.Api(api),
+        endpointDiscoveryEnabled: true,
+        endpoint: 'custom-endpoint.amazonaws.com/fack-region',
+      });
+      var error;
+      try {
+        client.makeRequest('optionalEDOperation', {Query: 'query'}).build();
+      } catch(e) {
+        error = e;
+      }
+      expect(error).not.to.eql(null);
+      expect(error.code).to.eql('ConfigurationException');
+      expect(error.message).to.eql('Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.');
+  
+      client = new AWS.Service({
+        apiConfig: new AWS.Model.Api(api),
+        endpoint: 'custom-endpoint.amazonaws.com/fake-region',
+      });
+      var getFromCacheSpy = helpers.spyOn(AWS.endpointCache, 'get').andCallThrough();
+      client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'}).send();
+      expect(getFromCacheSpy.calls.length).to.eql(0);
+    });
+  
+    it('if endpoint specified in global config, custom endpoint should be preferred', function() {
+      AWS.config.update({endpoint: 'custom-endpoint.amazonaws.com/fake-region'})
+      client = new AWS.Service({
+        apiConfig: new AWS.Model.Api(api),
+      });
+      var getFromCacheSpy = helpers.spyOn(AWS.endpointCache, 'get').andCallThrough();
+      client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'}).send();
+      expect(getFromCacheSpy.calls.length).to.eql(0);
+      delete AWS.config.endpoint;
+    });
+  
+    it('if endpoint specified in global config, custom endpoint should be preferred', function() {
+      AWS.config.update({mock: {endpoint: 'custom-endpoint.amazonaws.com/fake-region'}})
+      var MockService = helpers.MockServiceFromApi(api);
+      var client = new MockService({});
+      var getFromCacheSpy = helpers.spyOn(AWS.endpointCache, 'get').andCallThrough();
+      client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'}).send();
+      expect(getFromCacheSpy.calls.length).to.eql(0);
+      delete AWS.config.mock;
+    });
+  
+    it('append "endpoint-discovery" to user-agent on all requests', function() {
+      var client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api),
+      });
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+      request.send();
+      if(AWS.util.isNode()) {
+        expect(request.httpRequest.headers['User-Agent']).include('endpoint-discovery');
+      } else {
+        expect(request.httpRequest.headers['X-Amz-User-Agent']).include('endpoint-discovery');
+      }
+
+      request = client.makeRequest('optionalEDOperation', {Query: 'query'});
+      request.send();
+      if(AWS.util.isNode()) {
+        expect(request.httpRequest.headers['User-Agent']).include('endpoint-discovery');
+      } else {
+        expect(request.httpRequest.headers['X-Amz-User-Agent']).include('endpoint-discovery');
+      }
+    });
+  });
+
+  describe('configurations', function() {
+    var env;
+    beforeEach(function() {
+      env = process.env;
+      process.env = {};
+      AWS.endpointCache.empty();
+    });
+    afterEach(function() {
+      process.env = env;
+    });
+
+    it('default to turnoff endpoint discovery', function() {
+      client = new AWS.Service({
+        apiConfig: new AWS.Model.Api(api),
+      });
+      var spy = helpers.createSpy('send inject');
+      AWS.events.on('sign', function(req) {
+        if(req.operation === 'describeEndpoints') {
+          spy(req);
+        }
+      })
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      client.makeRequest('requiredEDOperation',  {Query: 'query', Record: 'record'}).send();
+      expect(spy.calls.length).to.eql(0);
+    })
+
+    it('turn on endpoint discovery in client config', function() {
+
+      client = new AWS.Service({
+        endpointDiscoveryEnabled: true,
+        apiConfig: new AWS.Model.Api(api),
+      });
+      var spy = helpers.createSpy('send inject');
+      AWS.events.on('sign', function(req) {
+        if(req.operation === 'describeEndpoints') {
+          spy(req);
+        }
+      });
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      expect(spy.calls.length).to.eql(1);
+      client.makeRequest('requiredEDOperation',  {Query: 'query', Record: 'record'}).send();
+      expect(spy.calls.length).to.eql(2);
+    });
+
+    it('turn on endpoint discovery in global config', function() {
+      AWS.config.endpointDiscoveryEnabled = true;
+      var client = new AWS.Service({
+        apiConfig: new AWS.Model.Api(api),
+      });
+      var spy = helpers.createSpy('send inject');
+      AWS.events.on('sign', function(req) {
+        if(req.operation === 'describeEndpoints') {
+          spy(req);
+        }
+      });
+      helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+      client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+      expect(spy.calls.length).to.eql(1);
+      client.makeRequest('requiredEDOperation',  {Query: 'query', Record: 'record'}).send();
+      expect(spy.calls.length).to.eql(2);
+    });
+
+    if (AWS.util.isNode()) {
+      it('turn on endpoint discovery from environmental variable', function() {
+        var client = new AWS.Service({
+          endpointDiscoveryEnabled: false,
+          apiConfig: new AWS.Model.Api(api),
+        });
+        var spy = helpers.createSpy('send inject');
+        AWS.events.on('sign', function(req) {
+          if(req.operation === 'describeEndpoints') {
+            spy(req);
+          }
+        });
+        process.env.AWS_ENABLE_ENDPOINT_DISCOVERY = '1';
+        helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+        client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+        expect(spy.calls.length).to.eql(1);
+        client.makeRequest('requiredEDOperation',  {Query: 'query', Record: 'record'}).send();
+        expect(spy.calls.length).to.eql(2);
+
+        process.env.AWS_ENABLE_ENDPOINT_DISCOVERY = undefined;
+        request = client.makeRequest('optionalEDOperation', {Query: 'query'});
+        var error;
+        try {
+          request.send();
+        } catch(e) {
+          error = e;
+        }
+        expect(error).not.to.eql(undefined);
+        expect(error.code).to.eql('ConfigurationException');
+        expect(error.message).to.eql('environmental variable AWS_ENABLE_ENDPOINT_DISCOVERY cannot be set to nothing');
+      });
+  
+      it('turn on endpoint discovery from config file', function() {
+        var client = new AWS.Service({
+          endpointDiscoveryEnabled: false,
+          apiConfig: new AWS.Model.Api(api),
+        });
+        helpers.spyOn(iniLoader, 'loadFrom').andReturn({'dummyRole': {endpoint_discovery_enabled: true}});
+        process.env.AWS_PROFILE = 'dummyRole';
+        var spy = helpers.createSpy('send inject');
+        AWS.events.on('sign', function(req) {
+          if(req.operation === 'describeEndpoints') {
+            spy(req)
+          }
+        });
+        helpers.mockHttpResponse(200, {}, '{"Endpoints": [{"Address": "https://cell1.fakeservice.amazonaws.com/fakeregion", "CachePeriodInMinutes": 1}]}');
+        client.makeRequest('optionalEDOperation', {Query: 'query'}).send();
+        expect(spy.calls.length).to.eql(1);
+        client.makeRequest('requiredEDOperation',  {Query: 'query', Record: 'record'}).send();
+        expect(spy.calls.length).to.eql(2);
+
+        helpers.spyOn(iniLoader, 'loadFrom').andReturn({'dummyRole': {endpoint_discovery_enabled: undefined}});
+        var request = client.makeRequest('requiredEDOperation', {Query: 'query', Record: 'record'});
+        var error;
+        try {
+          request.send();
+        } catch(e) {
+          error = e;
+        }
+        expect(error).not.to.eql(undefined);
+        expect(error.code).to.eql('ConfigurationException');
+        expect(error.message).to.eql('config file entry \'endpoint_discovery_enabled\' cannot be set to nothing');
+      });
+    }
+  });
+});
\ No newline at end of file
diff --git a/test/helpers.js b/test/helpers.js
index 74dc8bd1c..4cb84d0bd 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -171,10 +171,11 @@
   });
 
   MockServiceFromApi = function(customApi) {
-    customApi.metadata = {
-      endpointPrefix: 'mockservice',
-      signatureVersion: 'v4'
-    };
+    if (!customApi.metadata) {
+      customApi.metadata = {};
+      customApi.metadata.endpointPrefix = 'mockservice';
+      customApi.metadata.signatureVersion = 'v4';
+    }
     return AWS.Service.defineService('mock', {
       serviceIdentifier: 'mock',
       initialize: function(config) {
@@ -183,18 +184,7 @@
           accessKeyId: 'akid',
           secretAccessKey: 'secret'
         };
-        return this.config.region = 'mock-region';
-      },
-      setupRequestListeners: function(request) {
-        request.on('extractData', function(resp) {
-          return resp.data = (resp.httpResponse.body || '').toString();
-        });
-        return request.on('extractError', function(resp) {
-          return resp.error = {
-            code: (resp.httpResponse.body || '').toString() || resp.httpResponse.statusCode,
-            message: null
-          };
-        });
+        this.config.region = this.config.region || 'mock-region';
       },
       api: new AWS.Model.Api(customApi)
     });
diff --git a/test/http_request.spec.js b/test/http_request.spec.js
index 5ab056cbf..34d706dc1 100644
--- a/test/http_request.spec.js
+++ b/test/http_request.spec.js
@@ -65,6 +65,22 @@
         return expect(request.search()).to.equal('mno=hjk');
       });
     });
+    describe('updateEndpoint', function() {
+      it('update request endpoint and path', function() {
+        request.updateEndpoint('https://another.com:5678/path/service?foo=3&foo.options=5');
+        expect(request.search()).to.equal('foo=3&foo.options=5');
+        expect(request.pathname()).to.equal('/path/service');
+        expect(request.path).to.equal('/path/service?foo=3&foo.options=5');
+        expect(request.endpoint.port).to.equal(5678);
+        expect(request.endpoint).to.include({
+          port: 5678,
+          hostname: 'another.com',
+          host: 'another.com:5678',
+          protocol: 'https:',
+          href: 'https://another.com:5678/path/service?foo=3&foo.options=5'
+        })
+      })
+    })
     describe('getUserAgentHeaderName', function() {
       it('provides the correct header for browser environments', function() {
         helpers.spyOn(AWS.util, 'isBrowser').andReturn(true);
diff --git a/test/model/api.spec.js b/test/model/api.spec.js
index ef0ebbb81..61a98fbcf 100644
--- a/test/model/api.spec.js
+++ b/test/model/api.spec.js
@@ -11,6 +11,7 @@
   };
 
   describe('AWS.Model.Api', function() {
+
     describe('loading', function() {
       return it('loads properties from metadata', function() {
         var api;
@@ -38,11 +39,13 @@
         return expect(api.signingName).to.equal('alias');
       });
     });
+
     describe('isApi', function() {
       return it('is an API', function() {
         return expect(make().isApi).to.equal(true);
       });
     });
+
     describe('className', function() {
       it('generates the correct class name from fullName', function() {
         var api;
@@ -75,6 +78,45 @@
         return expect(make().className).to.equal(null);
       });
     });
+
+    describe('endpoint operation', function() {
+      it('adds endpiont discovery opeation name when exists', function() {
+        var api = make({
+          operations: {
+            DescribeEndpoints: {
+              http: {},
+              input: {},
+              name: 'DescribeEndpoints',
+              endpointoperation: true,
+            },
+            SomeOperation: {
+              http: {},
+              input: {},
+              name: 'SomeOperation',
+            },
+            OptionalEndpointOperation: {
+              http: {},
+              input: {},
+              name: 'OptionalEndpointOperation',
+              endpointdiscovery: {}
+            },
+            RequiredEndpointOperation: {
+              http: {},
+              input: {},
+              name: 'RequiredEndpointOperation',
+              endpointdiscovery: {
+                required: true
+              }
+            }
+          }
+        });
+        expect(api.endpointOperation).to.equal('describeEndpoints');
+        expect(api.operations.someOperation.endpointDiscoveryRequired).to.equal('NULL');
+        expect(api.operations.optionalEndpointOperation.endpointDiscoveryRequired).to.equal('OPTIONAL');
+        expect(api.operations.requiredEndpointOperation.endpointDiscoveryRequired).to.equal('REQUIRED');
+      })
+    });
+
     describe('documentation', function() {
       it('does not provide documentation by default', function() {
         var api;
@@ -93,7 +135,8 @@
         return expect(api.documentation).to.equal('foo');
       });
     });
-    return describe('shapes', function() {
+
+    describe('shapes', function() {
       return it('creates a set of shapes', function() {
         var api;
         api = make({
diff --git a/test/publisher/configuration.spec.js b/test/publisher/configuration.spec.js
index 5cecc120a..1a0f76a6c 100644
--- a/test/publisher/configuration.spec.js
+++ b/test/publisher/configuration.spec.js
@@ -2,7 +2,7 @@ var helpers = require('../helpers');
 var AWS = helpers.AWS;
 var spyOn = helpers.spyOn;
 var monitoringConfig = require('../../lib/publisher/configuration');
-var iniLoader = require('../../lib/shared-ini').iniLoader;
+var iniLoader = AWS.util.iniLoader;
 
 if (AWS.util.isNode()) {
   describe('configuration resolving', function () {
@@ -72,6 +72,7 @@ if (AWS.util.isNode()) {
       it('should correctly parse truthy value to true', function () {
         var enabled = ['1', 'true', 'whatever', 'X', '99'];
         for (var i = 0; i < enabled.length; i++) {
+          iniLoader.clearCachedFiles();
           process.env.AWS_PROFILE = 'dummyRole';
           helpers.spyOn(AWS.util, 'readFileSync').andReturn('[profile dummyRole]\ncsm_enabled=' + enabled[i]);
           expect(monitoringConfig()).to.eql({
diff --git a/test/sequential_executor.spec.js b/test/sequential_executor.spec.js
index a04d4d0da..8528cb5f9 100644
--- a/test/sequential_executor.spec.js
+++ b/test/sequential_executor.spec.js
@@ -115,8 +115,12 @@
         this.emitter.addNamedListener('FUNCTION_4', 'event', function() {
           list.push(4);
         });
-        this.emitter.emit('event');
-        return expect(list).to.eql([3, 2, 1, 4]);
+        this.emitter.addNamedAsyncListener('FUNCTION_5', 'event', function(done) {
+          list.push(5);
+          done();
+        }, addToHead);
+        this.emitter.emit('event', []);
+        return expect(list).to.eql([5, 3, 2, 1, 4]);
       })
     });
 
@@ -205,4 +209,4 @@
     });
   });
 
-}).call(this);
+}).call(this);
\ No newline at end of file