Skip to content

Commit 3f6699f

Browse files
authored
fix(storage-s3): ensure s3 sockets are cleaned up (#11626)
Ensures all s3 sockets are cleaned up. Now passes through default request handler options that `@smithy/node-http-handler` now handles properly. Fixes #6382 ```ts const defaultRequestHandlerOpts: NodeHttpHandlerOptions = { httpAgent: { keepAlive: true, maxSockets: 100, }, httpsAgent: { keepAlive: true, maxSockets: 100, }, } ``` If you continue to have socket issues, you can customize any of the options by setting `requestHandler` property on your s3 config. This will take precedence if set. ```ts requestHandler: { httpAgent: { maxSockets: 300, keepAlive: true, }, httpsAgent: { maxSockets: 300, keepAlive: true, }, // Optional, only set these if you continue to see issues. Be wary of timeouts if you're dealing with large files. // time limit (ms) for receiving response. requestTimeout: 5_000, // time limit (ms) for establishing connection. connectionTimeout: 5_000, }), ```
1 parent 39d783a commit 3f6699f

File tree

4 files changed

+38
-16
lines changed

4 files changed

+38
-16
lines changed

packages/storage-s3/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@payloadcms/plugin-cloud-storage": "workspace:*"
5353
},
5454
"devDependencies": {
55+
"@smithy/node-http-handler": "4.0.3",
5556
"payload": "workspace:*"
5657
},
5758
"peerDependencies": {

packages/storage-s3/src/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
CollectionOptions,
66
GeneratedAdapter,
77
} from '@payloadcms/plugin-cloud-storage/types'
8+
import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'
89
import type { Config, Plugin, UploadCollectionSlug } from 'payload'
910

1011
import * as AWS from '@aws-sdk/client-s3'
@@ -64,16 +65,31 @@ export type S3StorageOptions = {
6465

6566
type S3StoragePlugin = (storageS3Args: S3StorageOptions) => Plugin
6667

68+
let storageClient: AWS.S3 | null = null
69+
70+
const defaultRequestHandlerOpts: NodeHttpHandlerOptions = {
71+
httpAgent: {
72+
keepAlive: true,
73+
maxSockets: 100,
74+
},
75+
httpsAgent: {
76+
keepAlive: true,
77+
maxSockets: 100,
78+
},
79+
}
80+
6781
export const s3Storage: S3StoragePlugin =
6882
(s3StorageOptions: S3StorageOptions) =>
6983
(incomingConfig: Config): Config => {
70-
let storageClient: AWS.S3 | null = null
71-
7284
const getStorageClient: () => AWS.S3 = () => {
7385
if (storageClient) {
7486
return storageClient
7587
}
76-
storageClient = new AWS.S3(s3StorageOptions.config ?? {})
88+
89+
storageClient = new AWS.S3({
90+
requestHandler: defaultRequestHandlerOpts,
91+
...(s3StorageOptions.config ?? {}),
92+
})
7793
return storageClient
7894
}
7995

packages/storage-s3/src/staticHandler.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ const isNodeReadableStream = (body: unknown): body is Readable => {
2424
)
2525
}
2626

27+
const destroyStream = (object: AWS.GetObjectOutput | undefined) => {
28+
if (object?.Body && isNodeReadableStream(object.Body)) {
29+
object.Body.destroy()
30+
}
31+
}
32+
2733
// Convert a stream into a promise that resolves with a Buffer
2834
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2935
const streamToBuffer = async (readableStream: any) => {
@@ -36,12 +42,13 @@ const streamToBuffer = async (readableStream: any) => {
3642

3743
export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
3844
return async (req, { params: { clientUploadContext, filename } }) => {
45+
let object: AWS.GetObjectOutput | undefined = undefined
3946
try {
4047
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
4148

4249
const key = path.posix.join(prefix, filename)
4350

44-
const object = await getStorageClient().getObject({
51+
object = await getStorageClient().getObject({
4552
Bucket: bucket,
4653
Key: key,
4754
})
@@ -54,7 +61,7 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
5461
const objectEtag = object.ETag
5562

5663
if (etagFromHeaders && etagFromHeaders === objectEtag) {
57-
const response = new Response(null, {
64+
return new Response(null, {
5865
headers: new Headers({
5966
'Accept-Ranges': String(object.AcceptRanges),
6067
'Content-Length': String(object.ContentLength),
@@ -63,13 +70,6 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
6370
}),
6471
status: 304,
6572
})
66-
67-
// Manually destroy stream before returning cached results to close socket
68-
if (object.Body && isNodeReadableStream(object.Body)) {
69-
object.Body.destroy()
70-
}
71-
72-
return response
7373
}
7474

7575
// On error, manually destroy stream to close socket
@@ -99,6 +99,8 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
9999
} catch (err) {
100100
req.payload.logger.error(err)
101101
return new Response('Internal Server Error', { status: 500 })
102+
} finally {
103+
destroyStream(object)
102104
}
103105
}
104106
}

pnpm-lock.yaml

Lines changed: 7 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)