@@ -28,7 +28,11 @@ 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 {
32
+ FatalError ,
33
+ IgnorableError ,
34
+ RecoverableError ,
35
+ } from "../utils/error.js" ;
32
36
import { debug , error } from "./logger.js" ;
33
37
import { optimizeImage } from "./plugins/image-optimization/image-optimization.js" ;
34
38
import { setNodeEnv } from "./util.js" ;
@@ -83,10 +87,8 @@ export async function defaultHandler(
83
87
// We return a 400 here if imageParams returns an errorMessage
84
88
// https://github.com/vercel/next.js/blob/512d8283054407ab92b2583ecce3b253c3be7b85/packages/next/src/server/next-server.ts#L937-L941
85
89
if ( "errorMessage" in imageParams ) {
86
- error (
87
- "Error during validation of image params" ,
88
- imageParams . errorMessage ,
89
- ) ;
90
+ const clientError = new RecoverableError ( imageParams . errorMessage , 400 ) ;
91
+ error ( "Error during validation of image params" , clientError ) ;
90
92
return buildFailureResponse (
91
93
imageParams . errorMessage ,
92
94
options ?. streamCreator ,
@@ -115,56 +117,17 @@ export async function defaultHandler(
115
117
) ;
116
118
return buildSuccessResponse ( result , options ?. streamCreator , etag ) ;
117
119
} catch ( e : any ) {
118
- // Determine if this is a client error (4xx) or server error (5xx)
119
- let statusCode = 500 ; // Default to 500 for unknown errors
120
- const isClientError =
121
- e &&
122
- typeof e === "object" &&
123
- ( ( "statusCode" in e &&
124
- typeof e . statusCode === "number" &&
125
- e . statusCode >= 400 &&
126
- e . statusCode < 500 ) ||
127
- e . code === "ENOTFOUND" ||
128
- e . code === "ECONNREFUSED" ||
129
- ( e . message &&
130
- ( e . message . includes ( "403" ) ||
131
- e . message . includes ( "404" ) ||
132
- e . message . includes ( "Access Denied" ) ||
133
- e . message . includes ( "Not Found" ) ) ) ) ;
120
+ // Determine if this is a client error (4xx) and convert it to appropriate error type
121
+ const classifiedError = classifyError ( e ) ;
134
122
135
- // Only log actual server errors as errors, log client errors as debug
136
- if ( isClientError ) {
137
- debug ( "Client error in image optimization" , e ) ;
138
- } else {
139
- error ( "Failed to optimize image" , e ) ;
140
- }
123
+ // Log with the appropriate level based on the error type
124
+ error ( "Image optimization error" , classifiedError ) ;
141
125
142
- // Determine appropriate status code based on error type
143
- if ( e && typeof e === "object" ) {
144
- if ( "statusCode" in e && typeof e . statusCode === "number" ) {
145
- statusCode = e . statusCode ;
146
- } else if ( "code" in e ) {
147
- const code = e . code as string ;
148
- if ( code === "ENOTFOUND" || code === "ECONNREFUSED" ) {
149
- statusCode = 404 ;
150
- }
151
- } else if ( e . message ) {
152
- if ( e . message . includes ( "403" ) || e . message . includes ( "Access Denied" ) ) {
153
- statusCode = 403 ;
154
- } else if (
155
- e . message . includes ( "404" ) ||
156
- e . message . includes ( "Not Found" )
157
- ) {
158
- statusCode = 404 ;
159
- }
160
- }
161
- }
162
-
163
- // Pass through the original error message from Next.js
126
+ // Pass through the error message from Next.js
164
127
return buildFailureResponse (
165
- e . message || "Internal server error" ,
128
+ classifiedError . message || "Internal server error" ,
166
129
options ?. streamCreator ,
167
- statusCode ,
130
+ classifiedError . statusCode ,
168
131
) ;
169
132
}
170
133
}
@@ -173,6 +136,63 @@ export async function defaultHandler(
173
136
// Helper functions //
174
137
//////////////////////
175
138
139
+ /**
140
+ * Classifies an error and converts it to the appropriate OpenNext error type
141
+ * with the correct status code and logging level.
142
+ */
143
+ function classifyError ( e : any ) : IgnorableError | RecoverableError | FatalError {
144
+ // Default values
145
+ let statusCode = 500 ;
146
+ const message = e ?. message || "Internal server error" ;
147
+
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
+ // Determine if this is a client error (4xx) or server error (5xx)
154
+ const isClientError =
155
+ e &&
156
+ typeof e === "object" &&
157
+ ( ( "statusCode" in e &&
158
+ typeof e . statusCode === "number" &&
159
+ e . statusCode >= 400 &&
160
+ e . statusCode < 500 ) ||
161
+ e . code === "ENOTFOUND" ||
162
+ e . code === "ECONNREFUSED" ||
163
+ ( e . message &&
164
+ ( e . message . includes ( "403" ) ||
165
+ e . message . includes ( "404" ) ||
166
+ e . message . includes ( "Access Denied" ) ||
167
+ e . message . includes ( "Not Found" ) ) ) ) ;
168
+
169
+ // Determine appropriate status code based on error type
170
+ if ( e && typeof e === "object" ) {
171
+ if ( "statusCode" in e && typeof e . statusCode === "number" ) {
172
+ statusCode = e . statusCode ;
173
+ } else if ( "code" in e ) {
174
+ const code = e . code as string ;
175
+ if ( code === "ENOTFOUND" || code === "ECONNREFUSED" ) {
176
+ statusCode = 404 ;
177
+ }
178
+ } else if ( e . message ) {
179
+ if ( e . message . includes ( "403" ) || e . message . includes ( "Access Denied" ) ) {
180
+ statusCode = 403 ;
181
+ } else if ( e . message . includes ( "404" ) || e . message . includes ( "Not Found" ) ) {
182
+ statusCode = 404 ;
183
+ }
184
+ }
185
+ }
186
+
187
+ // Client errors (4xx) are wrapped as IgnorableError to prevent noise in monitoring
188
+ if ( isClientError || statusCode < 500 ) {
189
+ return new IgnorableError ( message , statusCode ) ;
190
+ }
191
+
192
+ // Server errors (5xx) are marked as FatalError to ensure proper monitoring
193
+ return new FatalError ( message , statusCode ) ;
194
+ }
195
+
176
196
function validateImageParams (
177
197
headers : OutgoingHttpHeaders ,
178
198
query ?: InternalEvent [ "query" ] ,
@@ -305,11 +325,24 @@ async function downloadHandler(
305
325
const request = https . get ( url , ( response ) => {
306
326
// Check for HTTP error status codes
307
327
if ( response . statusCode && response . statusCode >= 400 ) {
308
- error ( `Failed to get image: HTTP ${ response . statusCode } ` ) ;
328
+ // Create an IgnorableError with appropriate status code
329
+ const clientError = new IgnorableError (
330
+ response . statusMessage || `HTTP error ${ response . statusCode } ` ,
331
+ response . statusCode ,
332
+ ) ;
333
+
334
+ // Log the error using proper error logger to handle it correctly
335
+ error ( "Client error fetching image" , clientError , {
336
+ status : response . statusCode ,
337
+ statusText : response . statusMessage ,
338
+ url : url . href ,
339
+ } ) ;
340
+
309
341
res . statusCode = response . statusCode ;
310
342
res . end ( ) ;
311
343
return ;
312
344
}
345
+
313
346
// IncomingMessage is a Readable stream, not a Writable
314
347
// We need to pipe it directly to the response
315
348
response
@@ -320,8 +353,12 @@ async function downloadHandler(
320
353
}
321
354
res . end ( ) ;
322
355
} )
323
- . once ( "error" , ( pipeErr ) => {
324
- error ( "Failed to get image during piping" , pipeErr ) ;
356
+ . once ( "error" , ( pipeErr : Error ) => {
357
+ const clientError = new IgnorableError (
358
+ `Error during image piping: ${ pipeErr . message } ` ,
359
+ 400 ,
360
+ ) ;
361
+ error ( "Failed to get image during piping" , clientError ) ;
325
362
if ( ! res . headersSent ) {
326
363
res . statusCode = 400 ;
327
364
}
@@ -330,24 +367,25 @@ async function downloadHandler(
330
367
} ) ;
331
368
332
369
request . on ( "error" , ( err : Error & { code ?: string } ) => {
333
- // For network errors, these are typically client errors (bad URL, etc.)
334
- // so log as debug instead of error to avoid false alarms
370
+ // For network errors, convert to appropriate error type based on error code
335
371
const isClientError =
336
372
err . code === "ENOTFOUND" || err . code === "ECONNREFUSED" ;
337
-
338
- if ( isClientError ) {
339
- // Log the full error for debugging but don't expose it to the client
340
- debug ( "Client error fetching image" , {
341
- code : err . code ,
342
- message : err . message ,
343
- } ) ;
344
- res . statusCode = 404 ; // Not Found for DNS or connection errors
345
- } else {
346
- error ( "Failed to get image" , err ) ;
347
- res . statusCode = 400 ; // Bad Request for other errors
348
- }
349
-
350
- // Don't send the error message back to the client
373
+ const statusCode = isClientError ? 404 : 400 ;
374
+
375
+ // Create appropriate error type
376
+ const clientError = new IgnorableError (
377
+ err . message || `Error fetching image: ${ err . code || "unknown error" } ` ,
378
+ statusCode ,
379
+ ) ;
380
+
381
+ // Log with error function but it will be handled properly based on error type
382
+ error ( "Error fetching image" , clientError , {
383
+ code : err . code ,
384
+ message : err . message ,
385
+ url : url . href ,
386
+ } ) ;
387
+
388
+ res . statusCode = statusCode ;
351
389
res . end ( ) ;
352
390
} ) ;
353
391
}
0 commit comments