fix(payload-cloud): handle socket closures (#11113)
- Port #11015 to handle sockets - Fix `AccessDenied` errors to properly return 404 in specific scenarios - Add optional `debug` flag
This commit is contained in:
@@ -69,6 +69,7 @@ export const payloadCloudPlugin =
|
|||||||
getStaticHandler({
|
getStaticHandler({
|
||||||
cachingOptions: pluginOptions?.uploadCaching,
|
cachingOptions: pluginOptions?.uploadCaching,
|
||||||
collection,
|
collection,
|
||||||
|
debug: pluginOptions?.debug,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
import type { Readable } from 'stream'
|
||||||
|
|
||||||
import type { CollectionCachingConfig, PluginOptions, StaticHandler } from './types.js'
|
import type { CollectionCachingConfig, PluginOptions, StaticHandler } from './types.js'
|
||||||
|
|
||||||
@@ -8,6 +9,19 @@ import { getStorageClient } from './utilities/getStorageClient.js'
|
|||||||
interface Args {
|
interface Args {
|
||||||
cachingOptions?: PluginOptions['uploadCaching']
|
cachingOptions?: PluginOptions['uploadCaching']
|
||||||
collection: CollectionConfig
|
collection: CollectionConfig
|
||||||
|
debug?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard for NodeJS.Readable streams
|
||||||
|
const isNodeReadableStream = (body: unknown): body is Readable => {
|
||||||
|
return (
|
||||||
|
typeof body === 'object' &&
|
||||||
|
body !== null &&
|
||||||
|
'pipe' in body &&
|
||||||
|
typeof (body as any).pipe === 'function' &&
|
||||||
|
'destroy' in body &&
|
||||||
|
typeof (body as any).destroy === 'function'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert a stream into a promise that resolves with a Buffer
|
// Convert a stream into a promise that resolves with a Buffer
|
||||||
@@ -19,7 +33,7 @@ const streamToBuffer = async (readableStream: any) => {
|
|||||||
return Buffer.concat(chunks)
|
return Buffer.concat(chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHandler => {
|
export const getStaticHandler = ({ cachingOptions, collection, debug }: Args): StaticHandler => {
|
||||||
let maxAge = 86400 // 24 hours default
|
let maxAge = 86400 // 24 hours default
|
||||||
let collCacheConfig: CollectionCachingConfig | undefined
|
let collCacheConfig: CollectionCachingConfig | undefined
|
||||||
if (cachingOptions !== false) {
|
if (cachingOptions !== false) {
|
||||||
@@ -56,6 +70,19 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
|||||||
return new Response(null, { status: 404, statusText: 'Not Found' })
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On error, manually destroy stream to close socket
|
||||||
|
if (object.Body && isNodeReadableStream(object.Body)) {
|
||||||
|
const stream = object.Body
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
req.payload.logger.error({
|
||||||
|
err,
|
||||||
|
key,
|
||||||
|
msg: 'Error streaming S3 object, destroying stream',
|
||||||
|
})
|
||||||
|
stream.destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const bodyBuffer = await streamToBuffer(object.Body)
|
const bodyBuffer = await streamToBuffer(object.Body)
|
||||||
|
|
||||||
return new Response(bodyBuffer, {
|
return new Response(bodyBuffer, {
|
||||||
@@ -68,25 +95,32 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
|||||||
status: 200,
|
status: 200,
|
||||||
})
|
})
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
/**
|
// Handle each error explicitly
|
||||||
* If object key does not found, the getObject function attempts a ListBucket operation.
|
if (err instanceof Error) {
|
||||||
* Because of permissions, this will throw very specific error that we can catch and handle.
|
/**
|
||||||
*/
|
* Note: If AccessDenied comes back, it typically means that the object key is not found.
|
||||||
if (
|
* The AWS SDK throws this because it attempts an s3:ListBucket operation under the hood
|
||||||
err instanceof Error &&
|
* if it does not find the object key, which we have disallowed in our bucket policy.
|
||||||
err.name === 'AccessDenied' &&
|
*/
|
||||||
err.message?.includes('s3:ListBucket') &&
|
if (err.name === 'AccessDenied') {
|
||||||
'type' in err &&
|
req.payload.logger.warn({
|
||||||
err.type === 'S3ServiceException'
|
awsErr: debug ? err : err.name,
|
||||||
) {
|
collectionSlug: collection.slug,
|
||||||
req.payload.logger.error({
|
msg: `Requested file not found in cloud storage: ${params.filename}`,
|
||||||
collectionSlug: collection.slug,
|
params,
|
||||||
err,
|
requestedKey: key,
|
||||||
msg: `Requested file not found in cloud storage: ${params.filename}`,
|
})
|
||||||
params,
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||||
requestedKey: key,
|
} else if (err.name === 'NoSuchKey') {
|
||||||
})
|
req.payload.logger.warn({
|
||||||
return new Response(null, { status: 404, statusText: 'Not Found' })
|
awsErr: debug ? err : err.name,
|
||||||
|
collectionSlug: collection.slug,
|
||||||
|
msg: `Requested file not found in cloud storage: ${params.filename}`,
|
||||||
|
params,
|
||||||
|
requestedKey: key,
|
||||||
|
})
|
||||||
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.payload.logger.error({
|
req.payload.logger.error({
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ export interface PayloadCloudEmailOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginOptions {
|
export interface PluginOptions {
|
||||||
|
/**
|
||||||
|
* Enable additional debug logging
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
debug?: boolean
|
||||||
|
|
||||||
/** Payload Cloud Email
|
/** Payload Cloud Email
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": ["./test/_community/config.ts"],
|
"@payload-config": ["./test/payload-cloud/config.ts"],
|
||||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user