@@ -28,11 +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 {
32
- FatalError ,
33
- IgnorableError ,
34
- type RecoverableError ,
35
- } from "../utils/error.js" ;
31
+ import { IgnorableError } from "../utils/error.js" ;
36
32
import { debug , error } from "./logger.js" ;
37
33
import { optimizeImage } from "./plugins/image-optimization/image-optimization.js" ;
38
34
import { setNodeEnv } from "./util.js" ;
@@ -87,9 +83,10 @@ export async function defaultHandler(
87
83
// We return a 400 here if imageParams returns an errorMessage
88
84
// https://github.com/vercel/next.js/blob/512d8283054407ab92b2583ecce3b253c3be7b85/packages/next/src/server/next-server.ts#L937-L941
89
85
if ( "errorMessage" in imageParams ) {
90
- // Use IgnorableError for client-side validation issues (logLevel 0) to prevent monitoring alerts
91
- const clientError = new IgnorableError ( imageParams . errorMessage , 400 ) ;
92
- error ( "Error during validation of image params" , clientError ) ;
86
+ error (
87
+ "Error during validation of image params" ,
88
+ imageParams . errorMessage ,
89
+ ) ;
93
90
return buildFailureResponse (
94
91
imageParams . errorMessage ,
95
92
options ?. streamCreator ,
@@ -118,17 +115,19 @@ export async function defaultHandler(
118
115
) ;
119
116
return buildSuccessResponse ( result , options ?. streamCreator , etag ) ;
120
117
} catch ( e : any ) {
121
- // Determine if this is a client error (4xx) and convert it to appropriate error type
122
- const classifiedError = classifyError ( e ) ;
118
+ // All image-related errors should be treated as client errors
119
+ // Extract status code from error or default to 400 Bad Request
120
+ const statusCode = e . statusCode || 400 ;
121
+ const errorMessage = e . message || "Failed to process image request" ;
123
122
124
- // Log with the appropriate level based on the error type
125
- error ( "Image optimization error" , classifiedError ) ;
123
+ // Create an IgnorableError for proper monitoring classification
124
+ const clientError = new IgnorableError ( errorMessage , statusCode ) ;
125
+ error ( "Failed to optimize image" , clientError ) ;
126
126
127
- // Pass through the error message from Next.js
128
127
return buildFailureResponse (
129
- classifiedError . message || "Internal server error" ,
128
+ errorMessage ,
130
129
options ?. streamCreator ,
131
- classifiedError . statusCode ,
130
+ statusCode , // Use the appropriate status code (preserving original when available)
132
131
) ;
133
132
}
134
133
}
@@ -137,52 +136,6 @@ export async function defaultHandler(
137
136
// Helper functions //
138
137
//////////////////////
139
138
140
- /**
141
- * Classifies an error as either a client error (IgnorableError) or server error (FatalError)
142
- * to ensure proper logging behavior without triggering false monitoring alerts.
143
- *
144
- * The primary goal is to preserve the original error information while ensuring
145
- * client errors don't trigger monitoring alerts.
146
- */
147
- function classifyError ( e : any ) : IgnorableError | RecoverableError | FatalError {
148
- // If it's already an OpenNext error, return it directly
149
- if ( e && typeof e === "object" && "__openNextInternal" in e ) {
150
- return e ;
151
- }
152
-
153
- // Preserve the original message
154
- const message = e ?. message || "Internal server error" ;
155
-
156
- // Preserve the original status code if available, otherwise use a default
157
- let statusCode = 500 ;
158
- if (
159
- e &&
160
- typeof e === "object" &&
161
- "statusCode" in e &&
162
- typeof e . statusCode === "number"
163
- ) {
164
- statusCode = e . statusCode ;
165
- }
166
-
167
- // Simple check for client errors - anything with a 4xx status code
168
- // or common error codes that indicate client issues
169
- const isClientError =
170
- ( statusCode >= 400 && statusCode < 500 ) ||
171
- ( e &&
172
- typeof e === "object" &&
173
- ( e . code === "ENOTFOUND" ||
174
- e . code === "ECONNREFUSED" ||
175
- e . code === "ETIMEDOUT" ) ) ;
176
-
177
- // Wrap client errors as IgnorableError to prevent monitoring alerts
178
- if ( isClientError ) {
179
- return new IgnorableError ( message , statusCode ) ;
180
- }
181
-
182
- // Server errors are marked as FatalError to ensure proper monitoring
183
- return new FatalError ( message , statusCode ) ;
184
- }
185
-
186
139
function validateImageParams (
187
140
headers : OutgoingHttpHeaders ,
188
141
query ?: InternalEvent [ "query" ] ,
@@ -295,124 +248,140 @@ async function downloadHandler(
295
248
// directly.
296
249
debug ( "downloadHandler url" , url ) ;
297
250
298
- // Reads the output from the Writable and writes to the response
299
- const pipeRes = ( w : Writable , res : ServerResponse ) => {
300
- w . pipe ( res )
251
+ /**
252
+ * Helper function to handle image errors consistently with appropriate response
253
+ * @param e The error object
254
+ * @param res The server response object
255
+ * @param isInternalImage Whether the error is from an internal image (S3) or external image
256
+ */
257
+ function handleImageError (
258
+ e : any ,
259
+ res : ServerResponse ,
260
+ isInternalImage : boolean ,
261
+ ) {
262
+ let originalStatus = e . statusCode || e . $metadata ?. httpStatusCode || 500 ;
263
+ let message = e . message || "Failed to process image request" ;
264
+
265
+ // Special handling for S3 ListBucket permission errors
266
+ // AWS SDK v3 nests error details deeply within the error object
267
+ const isListBucketError =
268
+ ( message . includes ( "s3:ListBucket" ) && message . includes ( "AccessDenied" ) ) ||
269
+ e . error ?. message ?. includes ( "s3:ListBucket" ) ||
270
+ ( e . Code === "AccessDenied" && e . Message ?. includes ( "s3:ListBucket" ) ) ;
271
+
272
+ if ( isListBucketError ) {
273
+ message = "Image not found or access denied" ;
274
+ // For S3 ListBucket errors, ensure we're using 403 (the actual AWS error)
275
+ if ( originalStatus === 500 && e . $metadata ?. httpStatusCode === 403 ) {
276
+ originalStatus = 403 ;
277
+ }
278
+
279
+ // Log using IgnorableError to classify as client error
280
+ const clientError = new IgnorableError ( message , originalStatus ) ;
281
+ error ( "S3 ListBucket permission error" , clientError ) ;
282
+ } else {
283
+ // Log all other errors as client errors
284
+ const clientError = new IgnorableError ( message , originalStatus ) ;
285
+ error ( "Failed to process image" , clientError ) ;
286
+ }
287
+
288
+ // For external images, throw if not ListBucket error
289
+ // Next.js will preserve the status code for external images
290
+ if ( ! isInternalImage && ! isListBucketError ) {
291
+ const formattedError = new Error ( message ) ;
292
+ // @ts -ignore: Add statusCode property to Error
293
+ formattedError . statusCode = originalStatus >= 500 ? 400 : originalStatus ;
294
+ throw formattedError ;
295
+ }
296
+
297
+ // Different handling for internal vs external images
298
+ const finalStatus = originalStatus >= 500 ? 400 : originalStatus ;
299
+ res . statusCode = finalStatus ;
300
+
301
+ // For internal images, we want to trigger Next.js's "internal response invalid" message
302
+ if ( isInternalImage ) {
303
+ // For internal images, don't set Content-Type to trigger Next.js's default error handling
304
+ // This should result in "url parameter is valid but internal response is invalid"
305
+
306
+ // Still include error details in headers for debugging only
307
+ const errorMessage = isListBucketError ? "Access denied" : message ;
308
+ res . setHeader ( "x-nextjs-internal-error" , errorMessage ) ;
309
+ res . end ( ) ;
310
+ } else {
311
+ // For external images, maintain existing behavior with text/plain
312
+ res . setHeader ( "Content-Type" , "text/plain" ) ;
313
+
314
+ if ( isListBucketError ) {
315
+ res . end ( "Access denied" ) ;
316
+ } else {
317
+ res . end ( message ) ;
318
+ }
319
+ }
320
+ }
321
+
322
+ // Pipes data from a writable stream to the server response
323
+ function pipeStream (
324
+ stream : Writable ,
325
+ res : ServerResponse ,
326
+ isInternalImage : boolean ,
327
+ ) {
328
+ stream
329
+ . pipe ( res )
301
330
. once ( "close" , ( ) => {
302
331
res . statusCode = 200 ;
303
332
res . end ( ) ;
304
333
} )
305
334
. once ( "error" , ( err ) => {
306
- error ( "Failed to get image" , err ) ;
307
- res . statusCode = 400 ;
308
- res . end ( ) ;
335
+ error ( "Error streaming image data" , err ) ;
336
+ handleImageError ( err , res , isInternalImage ) ;
309
337
} ) ;
310
- } ;
338
+ }
311
339
340
+ // Main handler logic with clearer error paths
312
341
try {
313
- // Case 1: remote image URL => download the image from the URL
342
+ // EXTERNAL IMAGE HANDLING
314
343
if ( url . href . toLowerCase ( ) . match ( / ^ h t t p s ? : \/ \/ / ) ) {
315
- const request = https . get ( url , ( response ) => {
316
- // Check for HTTP error status codes
317
- if ( response . statusCode && response . statusCode >= 400 ) {
318
- // Create an IgnorableError with appropriate status code
319
- const clientError = new IgnorableError (
320
- response . statusMessage || `HTTP error ${ response . statusCode } ` ,
321
- response . statusCode ,
322
- ) ;
323
-
324
- // Log the error using proper error logger to handle it correctly
325
- error ( "Client error fetching image" , clientError , {
326
- status : response . statusCode ,
327
- statusText : response . statusMessage ,
328
- url : url . href ,
329
- } ) ;
330
-
331
- res . statusCode = response . statusCode ;
332
- res . end ( ) ;
333
- return ;
334
- }
335
-
336
- // IncomingMessage is a Readable stream, not a Writable
337
- // We need to pipe it directly to the response
338
- response
339
- . pipe ( res )
340
- . once ( "close" , ( ) => {
341
- if ( ! res . headersSent ) {
342
- res . statusCode = 200 ;
343
- }
344
- res . end ( ) ;
345
- } )
346
- . once ( "error" , ( pipeErr : Error ) => {
347
- const clientError = new IgnorableError (
348
- `Error during image piping: ${ pipeErr . message } ` ,
349
- 400 ,
350
- ) ;
351
- error ( "Failed to get image during piping" , clientError ) ;
352
- if ( ! res . headersSent ) {
353
- res . statusCode = 400 ;
354
- }
355
- res . end ( ) ;
356
- } ) ;
357
- } ) ;
358
-
359
- request . on ( "error" , ( err : Error & { code ?: string } ) => {
360
- // For network errors, convert to appropriate error type based on error code
361
- const isClientError =
362
- err . code === "ENOTFOUND" || err . code === "ECONNREFUSED" ;
363
- const statusCode = isClientError ? 404 : 400 ;
364
-
365
- // Create appropriate error type
366
- const clientError = new IgnorableError (
367
- err . message || `Error fetching image: ${ err . code || "unknown error" } ` ,
368
- statusCode ,
369
- ) ;
370
-
371
- // Log with error function but it will be handled properly based on error type
372
- error ( "Error fetching image" , clientError , {
373
- code : err . code ,
374
- message : err . message ,
375
- url : url . href ,
376
- } ) ;
377
-
378
- res . statusCode = statusCode ;
379
- res . end ( ) ;
380
- } ) ;
344
+ try {
345
+ pipeStream ( https . get ( url ) , res , false ) ;
346
+ } catch ( e : any ) {
347
+ handleImageError ( e , res , false ) ;
348
+ }
349
+ return ;
381
350
}
382
- // Case 2: local image => download the image from S3
383
- else {
384
- // Download image from S3
385
- // note: S3 expects keys without leading `/`
386
351
352
+ // INTERNAL IMAGE HANDLING (S3)
353
+ try {
387
354
const response = await loader . load ( url . href ) ;
388
355
356
+ // Handle empty response body
389
357
if ( ! response . body ) {
390
- throw new Error ( "Empty response body from the S3 request." ) ;
391
- }
358
+ const message = "Empty response body from the S3 request." ;
359
+ const clientError = new IgnorableError ( message , 400 ) ;
360
+ error ( "Empty response from S3" , clientError ) ;
392
361
393
- // @ts -ignore
394
- pipeRes ( response . body , res ) ;
362
+ res . statusCode = 400 ;
363
+ res . setHeader ( "Content-Type" , "text/plain" ) ;
364
+ res . end ( message ) ;
365
+ return ;
366
+ }
395
367
396
- // Respect the bucket file's content-type and cache-control
397
- // imageOptimizer will use this to set the results.maxAge
368
+ // Set headers from the response
398
369
if ( response . contentType ) {
399
370
res . setHeader ( "Content-Type" , response . contentType ) ;
400
371
}
401
372
if ( response . cacheControl ) {
402
373
res . setHeader ( "Cache-Control" , response . cacheControl ) ;
403
374
}
375
+
376
+ // Stream the image to the client
377
+ // @ts -ignore
378
+ pipeStream ( response . body , res , true ) ;
379
+ } catch ( e : any ) {
380
+ // Direct response for all internal image errors
381
+ handleImageError ( e , res , true ) ;
404
382
}
405
383
} catch ( e : any ) {
406
- // Use our centralized error classification function
407
- const classifiedError = classifyError ( e ) ;
408
-
409
- // Log error with appropriate level based on error type
410
- // This will automatically downgrade client errors to debug level
411
- error ( "Failed to download image" , classifiedError ) ;
412
-
413
- // Since we're in a middleware (adapter) called by Next.js's image optimizer,
414
- // we should throw the properly classified error to let Next.js handle it appropriately
415
- // The Next.js image optimizer will use the status code and normalize the error message
416
- throw classifiedError ;
384
+ // Catch-all for any unexpected errors
385
+ handleImageError ( e , res , true ) ;
417
386
}
418
387
}
0 commit comments