@@ -28,7 +28,7 @@ import { emptyReadableStream, toReadableStream } from "utils/stream.js";
28
28
import type { OpenNextHandlerOptions } from "types/overrides.js" ;
29
29
import { createGenericHandler } from "../core/createGenericHandler.js" ;
30
30
import { resolveImageLoader } from "../core/resolve.js" ;
31
- import { IgnorableError } from "../utils/error.js" ;
31
+ import { FatalError , IgnorableError } from "../utils/error.js" ;
32
32
import { debug , error } from "./logger.js" ;
33
33
import { optimizeImage } from "./plugins/image-optimization/image-optimization.js" ;
34
34
import { setNodeEnv } from "./util.js" ;
@@ -258,39 +258,18 @@ async function downloadHandler(
258
258
res : ServerResponse ,
259
259
isInternalImage : boolean ,
260
260
) {
261
- let originalStatus = e . statusCode || e . $metadata ?. httpStatusCode || 500 ;
262
- let message = e . message || "Failed to process image request" ;
263
-
264
- // Special handling for S3 ListBucket permission errors
265
- // AWS SDK v3 nests error details deeply within the error object
266
- const isListBucketError =
267
- ( message . includes ( "s3:ListBucket" ) && message . includes ( "AccessDenied" ) ) ||
268
- e . error ?. message ?. includes ( "s3:ListBucket" ) ||
269
- ( e . Code === "AccessDenied" && e . Message ?. includes ( "s3:ListBucket" ) ) ;
270
-
271
- if ( isListBucketError ) {
272
- message = "Image not found or access denied" ;
273
- // For S3 ListBucket errors, ensure we're using 403 (the actual AWS error)
274
- if ( originalStatus === 500 && e . $metadata ?. httpStatusCode === 403 ) {
275
- originalStatus = 403 ;
276
- }
261
+ const originalStatus = e . statusCode || e . $metadata ?. httpStatusCode || 500 ;
262
+ const message = e . message || "Failed to process image request" ;
277
263
278
- // Log using IgnorableError to classify as client error
279
- const clientError = new IgnorableError ( message , originalStatus ) ;
280
- error ( "S3 ListBucket permission error" , clientError ) ;
281
- } else {
282
- // Log all other errors as client errors
283
- const clientError = new IgnorableError ( message , originalStatus ) ;
284
- error ( "Failed to process image" , clientError ) ;
285
- }
264
+ // Log all other errors as client errors
265
+ const clientError = new IgnorableError ( message , originalStatus ) ;
266
+ error ( "Failed to process image" , clientError ) ;
286
267
287
- // For external images, throw if not ListBucket error
268
+ // For external images we throw with the status code
288
269
// Next.js will preserve the status code for external images
289
- if ( ! isInternalImage && ! isListBucketError ) {
290
- const formattedError = new Error ( message ) ;
291
- // @ts -ignore: Add statusCode property to Error
292
- formattedError . statusCode = originalStatus >= 500 ? 400 : originalStatus ;
293
- throw formattedError ;
270
+ if ( ! isInternalImage ) {
271
+ const statusCode = originalStatus >= 500 ? 400 : originalStatus ;
272
+ throw new FatalError ( message , statusCode ) ;
294
273
}
295
274
296
275
// Different handling for internal vs external images
@@ -303,18 +282,15 @@ async function downloadHandler(
303
282
// This should result in "url parameter is valid but internal response is invalid"
304
283
305
284
// Still include error details in headers for debugging only
306
- const errorMessage = isListBucketError ? "Access denied" : message ;
307
- res . setHeader ( "x-nextjs-internal-error" , errorMessage ) ;
285
+ res . setHeader ( "x-nextjs-internal-error" , message ) ;
308
286
res . end ( ) ;
309
287
} else {
310
288
// For external images, maintain existing behavior with text/plain
311
289
res . setHeader ( "Content-Type" , "text/plain" ) ;
312
290
313
- if ( isListBucketError ) {
314
- res . end ( "Access denied" ) ;
315
- } else {
316
- res . end ( message ) ;
317
- }
291
+ // We should **never** send the error message to the client
292
+ // This is to prevent leaking sensitive information
293
+ res . end ( "Failed to process image request" ) ;
318
294
}
319
295
}
320
296
0 commit comments