@@ -28,7 +28,7 @@ import { emptyReadableStream, toReadableStream } from "utils/stream.js";
2828import type { OpenNextHandlerOptions } from "types/overrides.js" ;
2929import { createGenericHandler } from "../core/createGenericHandler.js" ;
3030import { resolveImageLoader } from "../core/resolve.js" ;
31- import { IgnorableError } from "../utils/error.js" ;
31+ import { FatalError , IgnorableError } from "../utils/error.js" ;
3232import { debug , error } from "./logger.js" ;
3333import { optimizeImage } from "./plugins/image-optimization/image-optimization.js" ;
3434import { setNodeEnv } from "./util.js" ;
@@ -258,39 +258,18 @@ async function downloadHandler(
258258 res : ServerResponse ,
259259 isInternalImage : boolean ,
260260 ) {
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" ;
277263
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 ) ;
286267
287- // For external images, throw if not ListBucket error
268+ // For external images we throw with the status code
288269 // 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 ) ;
294273 }
295274
296275 // Different handling for internal vs external images
@@ -303,18 +282,15 @@ async function downloadHandler(
303282 // This should result in "url parameter is valid but internal response is invalid"
304283
305284 // 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 ) ;
308286 res . end ( ) ;
309287 } else {
310288 // For external images, maintain existing behavior with text/plain
311289 res . setHeader ( "Content-Type" , "text/plain" ) ;
312290
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" ) ;
318294 }
319295 }
320296
0 commit comments