diff --git a/README.md b/README.md index 44910f7444a0..35bb64f73896 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Here’s an example of logging requests using middleware: const client = new DynamoDB({ region: "us-west-2" }); client.middlewareStack.add( - (next, context) => (args) => { + (next, context) => async (args) => { console.log("AWS SDK context", context.clientName, context.commandName); console.log("AWS SDK request input", args.input); const result = await next(args); @@ -533,7 +533,7 @@ const client = new S3({ region: "us-west-2" }); // Middleware added to client, applies to all commands. client.middlewareStack.add( - (next, context) => (args) => { + (next, context) => async (args) => { args.request.headers["x-amz-meta-foo"] = "bar"; console.log("AWS SDK context", context.clientName, context.commandName); console.log("AWS SDK request input", args.input); diff --git a/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts b/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts index bef20f5f9710..7e9a37d9d0c4 100644 --- a/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts +++ b/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts @@ -1,6 +1,7 @@ -import { S3Client } from "@aws-sdk/client-s3"; -import { defaultProvider as credentialDefaultProvider } from "@aws-sdk/credential-provider-node"; +import { S3Client, S3ClientConfigType } from "@aws-sdk/client-s3"; +import { defaultProvider as credentialDefaultProvider, defaultProvider } from "@aws-sdk/credential-provider-node"; import { NODE_USE_ARN_REGION_CONFIG_OPTIONS } from "@aws-sdk/middleware-bucket-endpoint"; +import { S3ExpressIdentityProviderImpl } from "@aws-sdk/middleware-sdk-s3"; import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; import { defaultUserAgent } from "@aws-sdk/util-user-agent-node"; import { @@ -14,16 +15,17 @@ import { Hash } from "@smithy/hash-node"; import { readableStreamHasher as streamHasher } from "@smithy/hash-stream-node"; import { NODE_MAX_ATTEMPT_CONFIG_OPTIONS, NODE_RETRY_MODE_CONFIG_OPTIONS } from "@smithy/middleware-retry"; import { loadConfig as loadNodeConfig } from "@smithy/node-config-provider"; -import { NodeHttpHandler as RequestHandler, streamCollector } from "@smithy/node-http-handler"; +import { NodeHttpHandler, streamCollector } from "@smithy/node-http-handler"; import { loadConfigsForDefaultMode } from "@smithy/smithy-client"; import { EndpointV2 } from "@smithy/types"; import { parseUrl } from "@smithy/url-parser"; import { fromBase64, toBase64 } from "@smithy/util-base64"; import { calculateBodyLength } from "@smithy/util-body-length-node"; import { resolveDefaultsModeConfig } from "@smithy/util-defaults-mode-node"; -import { DEFAULT_RETRY_MODE } from "@smithy/util-retry"; +import { ConfiguredRetryStrategy, DEFAULT_RETRY_MODE, StandardRetryStrategy } from "@smithy/util-retry"; import { getAwsChunkedEncodingStream, sdkStreamMixin } from "@smithy/util-stream"; import { fromUtf8, toUtf8 } from "@smithy/util-utf8"; +import https from "https"; /** * Successful compilation indicates the client can be initialized @@ -33,14 +35,29 @@ export const initializeWithMaximalConfiguration = () => { const defaultsMode = resolveDefaultsModeConfig({}); const defaultConfigProvider = () => defaultsMode().then(loadConfigsForDefaultMode); - const s3 = new S3Client({ + const config: Required = { + // BEGIN user options + region: loadNodeConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS), + credentials: defaultProvider({}), endpoint: "endpoint", - customUserAgent: "aws-client-api-test-user-agent", - apiVersion: "2006-03-01", - base64Decoder: fromBase64, - base64Encoder: toBase64, - disableHostPrefix: false, - endpointProvider: () => null as unknown as EndpointV2, + requestHandler: new NodeHttpHandler({ + httpsAgent: new https.Agent({ + maxSockets: 200, + keepAlive: true, + }), + requestTimeout: 15000, + connectionTimeout: 6000, + }), + retryStrategy: + new StandardRetryStrategy(3) || + new ConfiguredRetryStrategy(3, (attempt) => { + return attempt * 1_000; + }), + retryMode: loadNodeConfig({ + ...NODE_RETRY_MODE_CONFIG_OPTIONS, + default: async () => (await defaultConfigProvider()).retryMode || DEFAULT_RETRY_MODE, + }), + maxAttempts: loadNodeConfig(NODE_MAX_ATTEMPT_CONFIG_OPTIONS), logger: { trace() {}, debug() {}, @@ -48,36 +65,60 @@ export const initializeWithMaximalConfiguration = () => { warn() {}, error() {}, }, + signer: new SignatureV4MultiRegion({ + service: "s3", + region: "us-west-2", + credentials: defaultProvider({}), + sha256: Hash.bind(null, "sha256"), + }), + useDualstackEndpoint: loadNodeConfig(NODE_USE_DUALSTACK_ENDPOINT_CONFIG_OPTIONS), + useFipsEndpoint: loadNodeConfig(NODE_USE_FIPS_ENDPOINT_CONFIG_OPTIONS), + customUserAgent: "aws-client-api-test-user-agent", + extensions: [], + tls: true, + disableHostPrefix: false, + signingRegion: "us-west-2", + // END user options + + // BEGIN internal options + apiVersion: "2006-03-01", serviceId: "S3", + runtime: "node", + systemClockOffset: 0, signerConstructor: SignatureV4MultiRegion, - signingEscapePath: false, + endpointProvider: () => null as unknown as EndpointV2, urlParser: parseUrl, - runtime: "node", + base64Decoder: fromBase64, + base64Encoder: toBase64, defaultsMode, bodyLengthChecker: calculateBodyLength, credentialDefaultProvider: credentialDefaultProvider, defaultUserAgentProvider: defaultUserAgent({ serviceId: "S3", clientVersion: "3.0.0-client-s3-interface-test" }), eventStreamSerdeProvider: eventStreamSerdeProvider, getAwsChunkedEncodingStream: getAwsChunkedEncodingStream, - maxAttempts: loadNodeConfig(NODE_MAX_ATTEMPT_CONFIG_OPTIONS), md5: Hash.bind(null, "md5"), - region: loadNodeConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS), - requestHandler: new RequestHandler(defaultConfigProvider), - retryMode: loadNodeConfig({ - ...NODE_RETRY_MODE_CONFIG_OPTIONS, - default: async () => (await defaultConfigProvider()).retryMode || DEFAULT_RETRY_MODE, - }), sdkStreamMixin: sdkStreamMixin, sha1: Hash.bind(null, "sha1"), sha256: Hash.bind(null, "sha256"), streamCollector: streamCollector, streamHasher: streamHasher, - useArnRegion: loadNodeConfig(NODE_USE_ARN_REGION_CONFIG_OPTIONS), - useDualstackEndpoint: loadNodeConfig(NODE_USE_DUALSTACK_ENDPOINT_CONFIG_OPTIONS), - useFipsEndpoint: loadNodeConfig(NODE_USE_FIPS_ENDPOINT_CONFIG_OPTIONS), utf8Decoder: fromUtf8, utf8Encoder: toUtf8, - }); + // END internal options + + // S3 specific options below + useAccelerateEndpoint: false, + useArnRegion: loadNodeConfig(NODE_USE_ARN_REGION_CONFIG_OPTIONS), + forcePathStyle: false, + disableMultiregionAccessPoints: false, + followRegionRedirects: false, + s3ExpressIdentityProvider: new S3ExpressIdentityProviderImpl("createSessionFn" as any), + disableS3ExpressSessionAuth: false, + useGlobalEndpoint: false, + signingEscapePath: false, + }; + + const s3 = new S3Client(config); return s3; }; diff --git a/supplemental-docs/CLIENTS.md b/supplemental-docs/CLIENTS.md new file mode 100644 index 000000000000..63e7fc0a3260 --- /dev/null +++ b/supplemental-docs/CLIENTS.md @@ -0,0 +1,526 @@ +# SDK Clients + +This document will outline some of the most common configurable constructor parameters of each AWS SDK Client class. + +As a refresher, the basic usage of an SDK Client is of this form, using Amazon S3 as an example: + +```ts +// Example: sending a command (tree-shaking compatible). +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; + +const s3Client = new S3Client({}); + +await s3Client.send( + new GetObjectCommand({ + Bucket: "", + Key: "", + }) +); +``` + +```ts +// Example: calling an equivalent method (no tree-shaking). +import { S3 } from "@aws-sdk/client-s3"; + +const s3 = new S3({}); + +await s3.getObject({ + Bucket: "", + Key: "", +}); +``` + +## Preface + +Before we begin, make note that typically the only required +inputs to an SDK Client are credentials and a region. And, even these can be supplied +from environment variables or a configuration file such that your code initialization +does not necessarily need to include them. + +See https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html. + +```ts +// region and credentials may be supplied by the +// ~/.aws/config and ~/.aws/credentials files. +new S3Client({}); +``` + +All constructor parameters described in this document shall be optional. + +## Common Client Constructor Parameters + +### Region `region` + +Regions are a core AWS concept, +see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html. + +In this SDK, the region may be supplied as a string or an async function +returning a string. + +```ts +// Example: setting the client region. +new S3Client({ + region: "us-west-2", +}); +``` + +For region, the function option may rarely be needed, +but as with many SDK constructor parameters, an `async` function returning the +same type is also accepted. + +```ts +// Example: setting the client region with a function. +new S3Client({ + region: async () => "us-west-2", +}); +``` + +One important consideration: some credential providers use separate clients, +and will usually expose a field called `clientConfig`. It's best to pass +a consistent region to both your `Client` and the credential provider. + +For a list of credential providers, see https://www.npmjs.com/package/@aws-sdk/credential-providers. + +```ts +// Example: setting inner credential client configuration region. +import { fromCognitoIdentity } from "@aws-sdk/credential-providers"; + +new S3Client({ + // region for S3. + region: "us-west-2", + + credentials: fromCognitoIdentity({ + clientConfig: { + // region for the Cognito client (inner). + region: "us-west-2", + }, + }), +}); +``` + +### Credentials `credentials` + +AWS Credentials are needed by the SDK to perform requests against the correct +account. They are usually made of fields including an access key and secret key. + +Our **credential providers** are functions that enable the retrieval of AWS credentials +from a list of sources. See https://www.npmjs.com/package/@aws-sdk/credential-providers for the complete list. + +When you initialize a client without any particular provider specified, +the `Node.js` default credential provider chain is used (also described in the above link). For browsers, there is no default credential provider. + +At the simplest level, you can provide a literal object containing your credentials, but +you would only do this in testing. **Avoid saving your actual credentials in any code that may be shared.** + +```ts +// Example: providing literal credentials. +import { fromCognitoIdentity } from "@aws-sdk/credential-providers"; + +const client = new S3Client({ + credentials: { + accessKeyId: "...", + secretAccessKey: "...", + sessionToken: "...", + }, +}); +``` + +In addition, any `async` function can be given that resolves to the same type of object. +This is what the SDK's credential provider factory functions do. + +```ts +// Example: providing function resolver for credentials. +import { fromCognitoIdentity } from "@aws-sdk/credential-providers"; + +const client = new S3Client({ + credentials: async () => { + // get credentials from any source. + return { + accessKeyId: "...", + secretAccessKey: "...", + sessionToken: "...", + }; + }, +}); +``` + +### Custom Endpoint `endpoint` + +Each SDK client, by default, resolves the target endpoint with rule-based system +whose base template for any given operation is included in the service model or schema. +At runtime any and all inputs are potentially read to resolve the final endpoint. + +Inputs used to resolve the endpoint for an operation include the region, FIPS/dual-stack options as mentioned above, and in some cases even request-specific parameters. + +You may override all that logic by providing a custom endpoint to the Client +constructor. The simplest form is a URL string. + +```ts +// Example: setting a custom endpoint. +new S3Client({ + endpoint: "http://localhost:8888", +}); +``` + +Additional types include [Endpoint](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-smithy-types/Interface/Endpoint/) and [EndpointV2](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-smithy-types/Interface/EndpointV2/), or an +`async` function returning either. + +```ts +// Example: setting a custom endpoint with a function that returns an Endpoint (type). +new S3Client({ + endpoint: async () => { + return { + hostname: "localhost", + path: "/", + protocol: "http", + port: 8888, + }; + }, +}); +``` + +For more information about these structural alternative endpoint types, +use your IDE's type hints or refer to the API documentation linked above. + +### Request Handler `requestHandler` + +The requestHandler is used in the final step of sending an SDK request and the first receiver of +the response before it is parsed. The default requestHandler differs between +`Node.js` runtime and browsers. + +#### Node.js `requestHandler` + +The Node.js default SDK requestHandler uses the `node:http` and `node:https` modules. +For wider support, this is the case even if Node.js `fetch` is implemented in your environment. + +Commonly configured options include the number of maxSockets, and timeouts. + +For maxSockets in Node.js, see also: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-configuring-maxsockets.html + +```ts +// Example: setting maxSockets and timeouts. +import { NodeHttpHandler } from "@smithy/node-http-handler"; +import https from "https"; + +new S3Client({ + requestHandler: new NodeHttpHandler({ + httpsAgent: new https.Agent({ + keepAlive: true, + maxSockets: 200, // default is 50 per client. + }), + + // time limit (ms) for receiving response. + requestTimeout: 15_000, + + // time limit (ms) for establishing connection. + connectionTimeout: 6_000, + }), +}); +``` + +A note on **socket exhaustion**: if you encounter an error that indicates +you have run out of sockets due to a high volume of requests flowing through +your SDK Client, there are two things to check: + +1. Ensure that the number value of `maxSockets` is high enough for your + throughput needs. +2. Because `keepAlive` is defaulted to `true`, if you acquire a streaming response, + such as `S3::getObject`'s `Body` field. You must read the stream to completion + in order for the socket to close naturally. + +```ts +// Example: reading a stream to allow socket closure. +import { S3 } from "@aws-sdk/client-s3"; + +const s3 = new S3({}); + +const getObjectResult = await s3.getObject({ + Bucket: "...", + Key: "...", +}); + +const bodyStream = getObjectResult.Body; + +// one-time transform. Reads the stream and allows socket to close. +const bodyAsString = await bodyStream.transformToString(); + +// throws an error on 2nd call, stream cannot be rewound. +const __error__ = await bodyStream.transformToString(); +``` + +#### Browsers' `requestHandler` + +The default browser requestHandler is `@aws-sdk/fetch-http-handler` and uses +the global `fetch` implementation. There is an alternate handler `@aws-sdk/xhr-http-handler`, +and some SDK clients use a WebSocket handler as well. + +```ts +// Example: browser request options. +import { FetchHttpHandler } from "@smithy/fetch-http-handler"; + +new S3Client({ + requestHandler: new FetchHttpHandler({ + requestTimeout: 30_000, + }), +}); +``` + +XHR may be used as an alternative. It has only been tested with S3, and the known +use-case is when needing finer-grain event data for uploading files. + +```ts +// Example: XHR event listener. +// XHR http handler is from @aws-sdk, while fetch-http-handler is from @smithy. +import { XhrHttpHandler } from "@aws-sdk/xhr-http-handler"; + +const xhrRequestHandler = new XhrHttpHandler({ + requestTimeout: 30_000, +}); + +xhrRequestHandler.on(XhrHttpHandler.EVENTS.UPLOAD_PROGRESS, (event) => { + // ... +}); + +new S3Client({ + requestHandler: xhrRequestHandler, +}); +``` + +### Retry Strategy `retryStrategy`, `retryMode`, `maxAttempts` + +The SDK's default retry strategy is based on exponential backoff, and only retries +what we consider transient errors. The two basic configuration options are +the `maxAttempts` count and the computation of the retry delay. + +See also: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-smithy-util-retry/ + +Please keep in mind, when specifying a `retryStrategy`, the values for +`retryMode` and `maxAttempts` will be ignored, since the `retryStrategy` +contains values for those two concepts. + +```ts +// Example: setting maxAttempts +import { StandardRetryStrategy } from "@smithy/util-retry"; + +// the two following are equivalent: +new S3Client({ + maxAttempts: 5, +}); +new S3Client({ + retryStrategy: new StandardRetryStrategy(5), +}); +``` + +```ts +// Example: setting maxAttempts and backoff computation +import { ConfiguredRetryStrategy } from "@smithy/util-retry"; + +new S3Client({ + retryStrategy: new ConfiguredRetryStrategy( + // attempts + 5, + + // backoff 1 additional second per attempt + (attempt: number) => 500 + attempt * 1_000 + ), +}); +``` + +A known shortcoming is that in the SDK's specification for retries, the underlying +error is not exposed to the `RetryStrategy`. We want to improve this in the future, +but for now, use a custom retry wrapper if you need to check precise details +of the SDK's error object. + +```ts +// Example: building your own retry wrapper + +import { S3, S3ServiceException } from "@aws-sdk/client-s3"; + +const s3 = new S3({}); + +const retryWrapper = async (operation: (input: I) => Promise): typeof operation => { + return async (input: I) => { + let attempts = 5; + let lastError: S3ServiceException; + while (--attempts > 0) { + try { + return await operation(input); + } catch (e: unknown) { + lastError = e as S3ServiceException; + + const httpStatusCode = lastError.$metadata.httpStatusCode; + const errorCode = lastError.name; + // you decide conditions to throw or retry + } + } + return operation(input); + }; +}; + +const getObject = retryWrapper(async (params: Parameters[0]) => s3.getObject(params)); + +await getObject({ + Bucket: "...", + Key: "...", +}); +``` + +As for `retryMode`, you should rarely need to use this option. +The only two options are `STANDARD`, which is default and does not need +to be specified, and `ADAPTIVE`, which is equivalent to using the `AdaptiveRetryStrategy`. + +```ts +// Example: setting retryMode +import { AdaptiveRetryStrategy } from "@smithy/util-retry"; + +// the following two are equivalent: +new S3Client({ + retryMode: "ADAPTIVE", +}); +new S3Client({ + retryStrategy: new AdaptiveRetryStrategy(async () => 5 // maxAttempts), +}); +``` + +The `AdaptiveRetryStrategy` is similar to the `StandardRetryStrategy`, but +contains an additional `RateLimiter`. For details inspect the source code at +https://github.com/smithy-lang/smithy-typescript/blob/main/packages/util-retry/src/AdaptiveRetryStrategy.ts. + +### Logger `logger` + +```ts +// Example: turning on SDK client logging, ignoring trace and debug output +// by using console methods but overriding trace/debug to empty functions. +new S3Client({ + logger: { + ...console, + debug(...args) {}, + trace(...args) {}, + }, +}); +``` + +Note: our logging interface may contain multiple arguments per invocation, +which is common to the JavaScript console object. If your logger only accepts +one argument, concatenate the arguments before passing them through. + +```ts +// Example: concatenating log parameters +new S3Client({ + logger: { + ...console, + info(...args) { + return yourLogger.info(args.join(" ")); + }, + }, +}); +``` + +For request data and metadata logging, if the SDK's log outputs are not enough, +use middleware to log as much request information as you need. + +```ts +// Example: logging full request data and metadata. +import { S3 } from "@aws-sdk/client-s3"; + +const client = new S3({ region: "us-west-2" }); + +client.middlewareStack.add( + (next, context) => async (args) => { + console.log("AWS SDK context", context.clientName, context.commandName); + // console.log("AWS SDK request input", args.input); + console.log("AWS SDK raw request", args.request); + const result = await next(args); + console.log("AWS SDK raw response", result.response); + console.log("AWS SDK response metadata:", result.output.$metadata); + // console.log("AWS SDK request output:", result.output); + return result; + }, + { + name: "MyMiddleware", + step: "build", + override: true, + } +); + +await client.listBuckets({}); +``` + +### Dual-stack `useDualstackEndpoint` + +This is a simple `boolean` setting that is present in most SDK Clients. +It is used in endpoint resolution. + +```ts +// Example: setting useDualstackEndpoint +new S3Client({ + useDualstackEndpoint: false, +}); +``` + +Refer to: + +- https://docs.aws.amazon.com/whitepapers/latest/ipv6-on-aws/scaling-the-dual-stack-network-design-in-aws.html +- https://docs.aws.amazon.com/vpc/latest/userguide/aws-ipv6-support.html + +### Federal Information Processing Standard (FIPS) `useFipsEndpoint` + +This is a simple `boolean` setting that is present in most SDK Clients. +It is used in endpoint resolution. + +```ts +// Example: setting useDualstackEndpoint +new S3Client({ + useFipsEndpoint: false, +}); +``` + +Refer to: + +- https://aws.amazon.com/compliance/fips/ + +### (Smithy) Runtime Extensions `extensions` + +TODO + +## Other constructor parameters not listed here + +There are also many `@internal` constructor parameters that are not described +in this document. + +**You should not modify or use them**, but they are overridable for advanced use-cases. +`@internal` fields may be subject to change. + +Some examples include dependency injection for default +implementations of components such as: + +- base64 encode/decode +- sha1 / sha256 / md5 hashing functions +- stream decoder +- utf8 encode/decode +- default credential provider +- default endpoint resolver +- default signer implementations (sigv4, sigv4a) +- URL parser + +Additional metadata and bookkeeping systems like: + +- strings for default user agent, apiVersion, serviceId, runtime name +- system clock offset + +## Service-specific constructor parameters + +Some AWS services have customized behavior when used with the AWS SDK. These +are implemented using the same middleware stacks present on clients that is +available to users for adding custom behavior or logging, as well as +by expanding the Client constructors with additional options as needed. + +See also https://aws.amazon.com/blogs/developer/middleware-stack-modular-aws-sdk-js/. + +### S3 + +TODO e.g. `followRegionRedirects` + +### SQS + +TODO e.g. `useQueueUrlAsEndpoint` diff --git a/supplemental-docs/README.md b/supplemental-docs/README.md new file mode 100644 index 000000000000..b869241cd05a --- /dev/null +++ b/supplemental-docs/README.md @@ -0,0 +1,11 @@ +## Supplemental documentation + +This folder contains handwritten documentation from the developers of this SDK to supplement the programmatically generated documentation. + +#### [Clients](./CLIENTS.md) + +Information about initializing an SDK client and common configurable constructor parameters. + +#### [Upgrading](../UPGRADING.md) + +Upgrading from AWS SDK for JavaScript (v2) (https://github.com/aws/aws-sdk-js).