|
| 1 | +# Effective Practices |
| 2 | + |
| 3 | +This section contains general recommendations from the AWS SDK for JavaScript team. |
| 4 | + |
| 5 | +The code examples are using imports from the AWS SDK S3 client. |
| 6 | + |
| 7 | +```ts |
| 8 | +import type { S3ClientConfig, S3ClientResolvedConfig } from "@aws-sdk/client-s3"; |
| 9 | +import { |
| 10 | + CreateBucketCommand, |
| 11 | + ListObjectsV2Command, |
| 12 | + PutObjectCommand, |
| 13 | + GetObjectCommand, |
| 14 | + ListDirectoryBucketsCommand, |
| 15 | + S3Client, |
| 16 | +} from "@aws-sdk/client-s3"; |
| 17 | +``` |
| 18 | + |
| 19 | +## Table of Contents |
| 20 | + |
| 21 | +<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) --> |
| 22 | + |
| 23 | +- [(1) Minimize creating new copies of AWS SDK Clients for multiple operation calls](#1-minimize-creating-new-copies-of-aws-sdk-clients-for-multiple-operation-calls) |
| 24 | +- [(2) Avoid reading or mutating the AWS SDK client configuration object after instantiating the client](#2-avoid-reading-or-mutating-the-aws-sdk-client-configuration-object-after-instantiating-the-client) |
| 25 | + - [Incompatible write](#incompatible-write) |
| 26 | + - [Incompatible read](#incompatible-read) |
| 27 | + - [Recommended alternatives](#recommended-alternatives) |
| 28 | +- [(3) Always read streaming responses to completion or discard them](#3-always-read-streaming-responses-to-completion-or-discard-them) |
| 29 | + |
| 30 | +<!-- TOC end --> |
| 31 | + |
| 32 | +### (1) Minimize creating new copies of AWS SDK Clients for multiple operation calls |
| 33 | + |
| 34 | +The following example creates a client prior to making a request. |
| 35 | + |
| 36 | +```ts |
| 37 | +// ⚠️ |
| 38 | +for (const item of items) { |
| 39 | + const client = new S3Client({ |
| 40 | + region, |
| 41 | + credentials, |
| 42 | + }); |
| 43 | + await client.send(new PutObjectCommand(item)); |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +In cases where the operations being called are to the same client configuration, i.e. the same region |
| 48 | +and with the same credentials, creating a new client is unnecessary. It adds additional work for the calling program, |
| 49 | +since the client may need to re-compute credentials, endpoints, and other components needed to make a request. |
| 50 | + |
| 51 | +Our recommended way is to create one client per set of credentials and region, and reuse it for |
| 52 | +multiple commands. |
| 53 | + |
| 54 | +```ts |
| 55 | +// ✅ |
| 56 | +const client = new S3Client({ |
| 57 | + region, |
| 58 | + credentials, |
| 59 | +}); |
| 60 | + |
| 61 | +await client.send(new CreateBucketCommand({ Bucket })); |
| 62 | + |
| 63 | +for (const item of items) { |
| 64 | + await client.send(new PutObjectCommand(item)); |
| 65 | +} |
| 66 | + |
| 67 | +const objects = await client.send(new ListObjectsV2Command({ Bucket })); |
| 68 | +``` |
| 69 | + |
| 70 | +If you find a need to create a new client because of capacity issues with a single client, |
| 71 | +see [parallel workloads in Node.js](./performance/parallel-workloads-node-js.md). |
| 72 | + |
| 73 | +### (2) Avoid reading or mutating the AWS SDK client configuration object after instantiating the client |
| 74 | + |
| 75 | +Although the TypeScript interface of AWS SDK Clients contain a `public readonly config` field, |
| 76 | +we discourage making use of this field in any way, including reading and writing values. |
| 77 | + |
| 78 | +For backwards compatibility, we cannot make the field `private` or recursively `readonly`, but we'll explain |
| 79 | +below why the field should be ignored. |
| 80 | + |
| 81 | +#### Incompatible write |
| 82 | + |
| 83 | +```ts |
| 84 | +// ⚠️ |
| 85 | +import { ListObjectsV2Command } from "@aws-sdk/client-s3"; |
| 86 | + |
| 87 | +const client = new S3Client({ |
| 88 | + region, |
| 89 | + credentials, |
| 90 | +}); |
| 91 | + |
| 92 | +// ⚠️ incompatible mutation, will cause an error to be thrown later when calling operations. |
| 93 | +client.config.region = "us-west-2"; |
| 94 | + |
| 95 | +await client.send(new ListObjectsV2Command({ Bucket })); |
| 96 | +// ⚠️ Uncaught TypeError: config.region is not a function |
| 97 | +``` |
| 98 | + |
| 99 | +The `client.config` field is not a direct reference to the object that you pass into the S3Client constructor. |
| 100 | +It undergoes a process we call config resolution, in which many input fields are wrapped in normalizing functions. |
| 101 | + |
| 102 | +Whereas the constructor input has the type `S3ClientConfig`, the `client.config` object has the type |
| 103 | +`S3ClientResolvedConfig`, which is substantially transformed. |
| 104 | + |
| 105 | +For example, a `region` string of `"us-east-1"` becomes a function, or "provider", in the form of: |
| 106 | + |
| 107 | +```ts |
| 108 | +config.region = async () => "us-east-1"; |
| 109 | +``` |
| 110 | + |
| 111 | +Even more complex transforms are applied to config fields such as `credentials` and `signer`. Therefore, many |
| 112 | +`config` values which would be valid as constructor inputs cannot be written to the `client.config` object. |
| 113 | + |
| 114 | +#### Incompatible read |
| 115 | + |
| 116 | +Another example is attempting to determine an AWS service endpoint by using a client configured with a region. |
| 117 | + |
| 118 | +```ts |
| 119 | +// ⚠️ |
| 120 | +const client = new S3Client({ |
| 121 | + region, |
| 122 | + credentials, |
| 123 | +}); |
| 124 | + |
| 125 | +// ⚠️ incompatible reading, will throw an error: Uncaught TypeError: client.config.endpoint is not a function |
| 126 | +const endpoint = await client.config.endpoint(); |
| 127 | +``` |
| 128 | + |
| 129 | +This may seem initially reasonable, since each regional AWS service typically has a set of endpoints of the pattern |
| 130 | +`{service}.{region}.amazonaws.com`. However, AWS services can configure endpoints that differ based on many factors, |
| 131 | +including down to the distinct operation being called and its inputs. Therefore, the canonical endpoint cannot be |
| 132 | +accurately given before the operation and operation inputs are known. |
| 133 | + |
| 134 | +For example, the AWS SDK's S3 client uses the bucket name in the hostname, an operation level parameter, and the |
| 135 | +DynamoDB client may try to use the account ID in the hostname, a value that is not known until credentials are resolved |
| 136 | +during the first request. Endpoint variations are not limited to these examples. |
| 137 | + |
| 138 | +#### Recommended alternatives |
| 139 | + |
| 140 | +If you need to change regions, instantiate additional clients per region. They can share credentials to avoid duplicate |
| 141 | +credential resolution calls. |
| 142 | + |
| 143 | +```ts |
| 144 | +// ✅ |
| 145 | +import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; |
| 146 | + |
| 147 | +const credentialProvider = fromTemporaryCredentials(); |
| 148 | + |
| 149 | +const s3 = { |
| 150 | + east: new S3Client({ region: "us-east-1", credentials: credentialProvider }), |
| 151 | + west: new S3Client({ region: "us-west-2", credentials: credentialProvider }), |
| 152 | +}; |
| 153 | + |
| 154 | +const directoryEast = await s3.east.send(new ListDirectoryBucketsCommand()); |
| 155 | +const directoryWest = await s3.west.send(new ListDirectoryBucketsCommand()); |
| 156 | +``` |
| 157 | + |
| 158 | +If you want to know the resolved endpoint for an SDK operation, use the following helper function. |
| 159 | +You must provide the same Command constructor and input parameters as you would call, since those values are involved in |
| 160 | +determining the endpoint. |
| 161 | + |
| 162 | +```ts |
| 163 | +import { getEndpointFromInstructions } from "@smithy/middleware-endpoint"; |
| 164 | + |
| 165 | +// ✅ |
| 166 | +const operationParams = { |
| 167 | + Bucket, |
| 168 | + Key, |
| 169 | +}; |
| 170 | +const config = { |
| 171 | + region: "us-west-2", |
| 172 | + useDualstackEndpoint: false, |
| 173 | + useFipsEndpoint: false, |
| 174 | +}; |
| 175 | +const client = new S3Client(config); |
| 176 | + |
| 177 | +const endpoint = await getEndpointFromInstructions(operationParams, GetObjectCommand, config, { |
| 178 | + // logger: console, |
| 179 | +}); |
| 180 | + |
| 181 | +console.log(endpoint.url.toString()); |
| 182 | +``` |
| 183 | + |
| 184 | +### (3) Always read streaming responses to completion or discard them |
| 185 | + |
| 186 | +Some operations, the most common of which |
| 187 | +is [GetObjectCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/command/GetObjectCommand/), |
| 188 | +return a byte stream. |
| 189 | + |
| 190 | +Although awaiting such a request will return an HTTP status code and response headers, |
| 191 | + |
| 192 | +```ts |
| 193 | +const getObjectResponse = client.send(GetObjectCommand({ Bucket, Key })); |
| 194 | + |
| 195 | +console.log(getObjectResponse.$metadata.httpStatusCode); |
| 196 | +// ⚠️ byte stream is unhandled, leaving a socket in use. |
| 197 | +``` |
| 198 | + |
| 199 | +the request is incomplete. The connection will remain open until the byte stream, or payload, is read or discarded. |
| 200 | +Not doing so will leave the connection open, and in Node.js this can lead to a condition we call socket exhaustion. In |
| 201 | +the worst cases this can cause your application to slow, leak memory, and/or deadlock. |
| 202 | + |
| 203 | +We cannot automatically handle this for you. Since handling of the byte stream is application-dependent, we cannot infer |
| 204 | +your application's intent. In some cases there is an intentional delay in reading the byte stream, so we will not throw |
| 205 | +an Error if the stream is not immediately read. |
| 206 | + |
| 207 | +To handle the byte stream, use one of our built-in collection methods, pipe it somewhere such as a file or another S3 |
| 208 | +destination, or discard the stream. |
| 209 | + |
| 210 | +```ts |
| 211 | +// Caution: only do one of the following, because streams can only be read once: |
| 212 | +if (case1) { |
| 213 | + // ✅ buffer the stream |
| 214 | + const bytes = await getObjectResponse.Body.transformToByteArray(); |
| 215 | +} else if (case2) { |
| 216 | + // ✅ pipe the stream elsewhere |
| 217 | + await s3Client.send( |
| 218 | + new PutObjectCommand({ |
| 219 | + Bucket, |
| 220 | + Key, |
| 221 | + Body: getObjectResponse.Body, |
| 222 | + }) |
| 223 | + ); |
| 224 | +} else { |
| 225 | + // ✅ discard the stream |
| 226 | + // because our stream type varies depending on your runtime platform, |
| 227 | + // .destroy() is used for Node.js Readable. |
| 228 | + // .cancel() is used for Web Streams' ReadableStream. |
| 229 | + await(getObjectResponse.destroy?.() ?? getObjectResponse.cancel?.()); |
| 230 | +} |
| 231 | +``` |
0 commit comments