|
| 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 | +- [(4) Allow more time to establish connections when making requests cross-region](#4-allow-more-time-to-establish-connections-when-making-requests-cross-region) |
| 30 | + |
| 31 | +<!-- TOC end --> |
| 32 | + |
| 33 | +### (1) Minimize creating new copies of AWS SDK Clients for multiple operation calls |
| 34 | + |
| 35 | +The following example creates a new client in every iteration of the loop. |
| 36 | + |
| 37 | +```ts |
| 38 | +// ⚠️ |
| 39 | +for (const item of items) { |
| 40 | + const client = new S3Client({ |
| 41 | + region, |
| 42 | + credentials, |
| 43 | + }); |
| 44 | + await client.send(new PutObjectCommand(item)); |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +In cases where the operations being called are to the same client configuration, i.e. the same region |
| 49 | +and with the same credentials, creating a new client is not necessary. It increases work for the calling program, |
| 50 | +since the client may need to re-compute credentials, endpoints, and other components needed to make a request. |
| 51 | + |
| 52 | +Our recommended way is to create one client per set of credentials and region, and reuse it for |
| 53 | +multiple commands. |
| 54 | + |
| 55 | +```ts |
| 56 | +// ✅ |
| 57 | +const client = new S3Client({ |
| 58 | + region, |
| 59 | + credentials, |
| 60 | +}); |
| 61 | + |
| 62 | +await client.send(new CreateBucketCommand({ Bucket })); |
| 63 | + |
| 64 | +for (const item of items) { |
| 65 | + await client.send(new PutObjectCommand(item)); |
| 66 | +} |
| 67 | + |
| 68 | +const objects = await client.send(new ListObjectsV2Command({ Bucket })); |
| 69 | +``` |
| 70 | + |
| 71 | +If you need to create a new client because of capacity issues with a single client, |
| 72 | +see [parallel workloads in Node.js](./performance/parallel-workloads-node-js.md). |
| 73 | + |
| 74 | +### (2) Avoid reading or mutating the AWS SDK client configuration object after instantiating the client |
| 75 | + |
| 76 | +Although the TypeScript interface of AWS SDK Clients contain a `public readonly config` field, |
| 77 | +we discourage making use of this field in any way, including reading and writing values. |
| 78 | + |
| 79 | +For backwards compatibility, we cannot make the field `private` or recursively `readonly`, but we'll explain |
| 80 | +below why the field should be ignored. |
| 81 | + |
| 82 | +#### Incompatible write |
| 83 | + |
| 84 | +```ts |
| 85 | +// ⚠️ |
| 86 | +import { ListObjectsV2Command } from "@aws-sdk/client-s3"; |
| 87 | + |
| 88 | +const client = new S3Client({ |
| 89 | + region, |
| 90 | + credentials, |
| 91 | +}); |
| 92 | + |
| 93 | +// ⚠️ incompatible mutation, will cause an error to be thrown later when calling operations. |
| 94 | +client.config.region = "us-west-2"; |
| 95 | + |
| 96 | +await client.send(new ListObjectsV2Command({ Bucket })); |
| 97 | +// ⚠️ Uncaught TypeError: config.region is not a function |
| 98 | +``` |
| 99 | + |
| 100 | +The `client.config` field is not a direct reference to the object that you pass into the S3Client constructor. |
| 101 | +It undergoes a process we call config resolution, in which many input fields are wrapped in normalizing functions. |
| 102 | + |
| 103 | +Whereas the constructor input has the type `S3ClientConfig`, the `client.config` object has the type |
| 104 | +`S3ClientResolvedConfig`, which is substantially transformed. |
| 105 | + |
| 106 | +For example, a `region` string of `"us-east-1"` becomes a function, or "provider", in the form of: |
| 107 | + |
| 108 | +```ts |
| 109 | +config.region = async () => "us-east-1"; |
| 110 | +``` |
| 111 | + |
| 112 | +Even more complex transforms are applied to config fields such as `credentials` and `signer`. Therefore, many |
| 113 | +`config` values which would be valid as constructor inputs cannot be written to the `client.config` object. |
| 114 | + |
| 115 | +#### Incompatible read |
| 116 | + |
| 117 | +Another example is attempting to determine an AWS service endpoint by using a client configured with a region. |
| 118 | + |
| 119 | +```ts |
| 120 | +// ⚠️ |
| 121 | +const client = new S3Client({ |
| 122 | + region, |
| 123 | + credentials, |
| 124 | +}); |
| 125 | + |
| 126 | +// ⚠️ incompatible reading, will throw an error: Uncaught TypeError: client.config.endpoint is not a function |
| 127 | +const endpoint = await client.config.endpoint(); |
| 128 | +``` |
| 129 | + |
| 130 | +This may seem initially reasonable, since each regional AWS service typically has a set of endpoints of the pattern |
| 131 | +`{service}.{region}.amazonaws.com`. However, AWS services can configure endpoints that differ based on many factors, |
| 132 | +including down to the distinct operation being called and its inputs. Therefore, the canonical endpoint cannot be |
| 133 | +accurately given before the operation and operation inputs are known. |
| 134 | + |
| 135 | +For example, the AWS SDK's S3 client uses the bucket name in the hostname, an operation level parameter, and the |
| 136 | +DynamoDB client may try to use the account ID in the hostname, a value that is not known until credentials are resolved |
| 137 | +during the first request. Endpoint variations are not limited to these examples. |
| 138 | + |
| 139 | +#### Recommended alternatives |
| 140 | + |
| 141 | +If you need to change regions, instantiate additional clients per region. They can share credentials to avoid duplicate |
| 142 | +credential resolution calls. |
| 143 | + |
| 144 | +```ts |
| 145 | +// ✅ |
| 146 | +import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; |
| 147 | + |
| 148 | +const credentialProvider = fromTemporaryCredentials(); |
| 149 | + |
| 150 | +const s3 = { |
| 151 | + east: new S3Client({ region: "us-east-1", credentials: credentialProvider }), |
| 152 | + west: new S3Client({ region: "us-west-2", credentials: credentialProvider }), |
| 153 | +}; |
| 154 | + |
| 155 | +const directoryEast = await s3.east.send(new ListDirectoryBucketsCommand()); |
| 156 | +const directoryWest = await s3.west.send(new ListDirectoryBucketsCommand()); |
| 157 | +``` |
| 158 | + |
| 159 | +If you want to know the resolved endpoint for an SDK operation, use the following helper function. |
| 160 | +You must provide the same Command constructor and input parameters as you would call, since those values are involved in |
| 161 | +determining the endpoint. |
| 162 | + |
| 163 | +```ts |
| 164 | +import { getEndpointFromInstructions } from "@smithy/middleware-endpoint"; |
| 165 | + |
| 166 | +// ✅ |
| 167 | +const operationParams = { |
| 168 | + Bucket, |
| 169 | + Key, |
| 170 | +}; |
| 171 | +const config = { |
| 172 | + region: "us-west-2", |
| 173 | + useDualstackEndpoint: false, |
| 174 | + useFipsEndpoint: false, |
| 175 | +}; |
| 176 | +const client = new S3Client(config); |
| 177 | + |
| 178 | +const endpoint = await getEndpointFromInstructions(operationParams, GetObjectCommand, config, { |
| 179 | + // logger: console, |
| 180 | +}); |
| 181 | + |
| 182 | +console.log(endpoint.url.toString()); |
| 183 | +``` |
| 184 | + |
| 185 | +### (3) Always read streaming responses to completion or discard them |
| 186 | + |
| 187 | +Some operations, the most common of which |
| 188 | +is [GetObjectCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/command/GetObjectCommand/), |
| 189 | +return a byte stream. |
| 190 | + |
| 191 | +Although awaiting such a request will return an HTTP status code and response headers, |
| 192 | + |
| 193 | +```ts |
| 194 | +const getObjectResponse = await client.send(GetObjectCommand({ Bucket, Key })); |
| 195 | + |
| 196 | +console.log(getObjectResponse.$metadata.httpStatusCode); |
| 197 | +// ⚠️ byte stream is unhandled, leaving a socket in use. |
| 198 | +``` |
| 199 | + |
| 200 | +Although the API call is performed, and you have access to response, the connection will remain open until the byte |
| 201 | +stream, or payload, is read or discarded. |
| 202 | +Not doing so will leave the connection open, and in Node.js this can lead to a condition we call socket exhaustion. In |
| 203 | +the worst cases this can cause your application to slow, leak memory, and/or deadlock. |
| 204 | + |
| 205 | +We cannot automatically handle this for you. Since handling of the byte stream is application-dependent, we cannot infer |
| 206 | +your application's intent. In some cases there is an intentional delay in reading the byte stream, so we will not throw |
| 207 | +an Error if the stream is not immediately read. |
| 208 | + |
| 209 | +To handle the byte stream, use one of our built-in collection methods, pipe it somewhere such as a file or another S3 |
| 210 | +destination, or discard the stream. |
| 211 | + |
| 212 | +```ts |
| 213 | +// Caution: only do one of the following, because streams can only be read once: |
| 214 | +if (case1) { |
| 215 | + // ✅ buffer the stream |
| 216 | + const bytes = await getObjectResponse.Body.transformToByteArray(); |
| 217 | +} else if (case2) { |
| 218 | + // ✅ pipe the stream elsewhere |
| 219 | + await s3Client.send( |
| 220 | + new PutObjectCommand({ |
| 221 | + Bucket, |
| 222 | + Key, |
| 223 | + Body: getObjectResponse.Body, |
| 224 | + }) |
| 225 | + ); |
| 226 | +} else { |
| 227 | + // ✅ discard the stream |
| 228 | + // because our stream type varies depending on your runtime platform, |
| 229 | + // .destroy() is used for Node.js Readable. |
| 230 | + // .cancel() is used for Web Streams' ReadableStream. |
| 231 | + await(getObjectResponse.destroy?.() ?? getObjectResponse.cancel?.()); |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +To identify _which_ operations contain byte stream response payloads, refer to our API documentation. In the |
| 236 | +"Example Syntax" section of each operation's API reference page, the field that constitutes a byte stream will be marked |
| 237 | +as such: |
| 238 | + |
| 239 | +```ts |
| 240 | +// { // GetObjectOutput |
| 241 | +// Body: "<SdkStream>", // see \@smithy/types -> StreamingBlobPayloadOutputTypes |
| 242 | +// ... other fields ... |
| 243 | +// }; |
| 244 | +``` |
| 245 | + |
| 246 | +in the same way |
| 247 | +as [GetObjectCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/command/GetObjectCommand/). |
| 248 | +The byte stream field will always be a top-level property of the response object. |
| 249 | + |
| 250 | +### (4) Allow more time to establish connections when making requests cross-region |
| 251 | + |
| 252 | +This is outside the AWS SDK interfaces but an important consideration when making cross-region requests in AWS when |
| 253 | +using the Node.js runtime. For Node.js v20 and later, there is an |
| 254 | +option for TCP connections called `autoSelectFamilyAttemptTimeout <number>`. |
| 255 | + |
| 256 | +The [documentation](https://nodejs.org/dist/latest-v20.x/docs/api/net.html#netsetdefaultautoselectfamilyattempttimeoutvalue) |
| 257 | +states: |
| 258 | + |
| 259 | +> The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address |
| 260 | +
|
| 261 | +The default value of 250ms may be too low for some cross-region pairs within AWS, like those that are on |
| 262 | +opposite sides of the world, or simply in conditions of low network speed. This may manifest as an `AggregateError` with |
| 263 | +code `ETIMEDOUT` in Node.js. |
| 264 | + |
| 265 | +To increase this value within your application, use a `node` launch parameter such as |
| 266 | +`--network-family-autoselection-attempt-timeout=500` or |
| 267 | +the `node:net` API: |
| 268 | + |
| 269 | +```ts |
| 270 | +import net from "node:net"; |
| 271 | + |
| 272 | +net.setDefaultAutoSelectFamilyAttemptTimeout(500); |
| 273 | +``` |
| 274 | + |
| 275 | +The content of this item is based on the author's reading of this reported |
| 276 | +issue: https://github.com/nodejs/node/issues/54359. |
0 commit comments