Skip to content

Commit e7a51a1

Browse files
committed
refactor: improve error handling in image optimization with status codes and error classification
1 parent 6d36c0a commit e7a51a1

File tree

2 files changed

+121
-73
lines changed

2 files changed

+121
-73
lines changed

packages/open-next/src/adapters/image-optimization-adapter.ts

Lines changed: 108 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ import { emptyReadableStream, toReadableStream } from "utils/stream.js";
2828
import type { OpenNextHandlerOptions } from "types/overrides.js";
2929
import { createGenericHandler } from "../core/createGenericHandler.js";
3030
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";
3236
import { debug, error } from "./logger.js";
3337
import { optimizeImage } from "./plugins/image-optimization/image-optimization.js";
3438
import { setNodeEnv } from "./util.js";
@@ -83,10 +87,8 @@ export async function defaultHandler(
8387
// We return a 400 here if imageParams returns an errorMessage
8488
// https://github.com/vercel/next.js/blob/512d8283054407ab92b2583ecce3b253c3be7b85/packages/next/src/server/next-server.ts#L937-L941
8589
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);
9092
return buildFailureResponse(
9193
imageParams.errorMessage,
9294
options?.streamCreator,
@@ -115,56 +117,17 @@ export async function defaultHandler(
115117
);
116118
return buildSuccessResponse(result, options?.streamCreator, etag);
117119
} 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);
134122

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);
141125

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
164127
return buildFailureResponse(
165-
e.message || "Internal server error",
128+
classifiedError.message || "Internal server error",
166129
options?.streamCreator,
167-
statusCode,
130+
classifiedError.statusCode,
168131
);
169132
}
170133
}
@@ -173,6 +136,63 @@ export async function defaultHandler(
173136
// Helper functions //
174137
//////////////////////
175138

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+
176196
function validateImageParams(
177197
headers: OutgoingHttpHeaders,
178198
query?: InternalEvent["query"],
@@ -305,11 +325,24 @@ async function downloadHandler(
305325
const request = https.get(url, (response) => {
306326
// Check for HTTP error status codes
307327
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+
309341
res.statusCode = response.statusCode;
310342
res.end();
311343
return;
312344
}
345+
313346
// IncomingMessage is a Readable stream, not a Writable
314347
// We need to pipe it directly to the response
315348
response
@@ -320,8 +353,12 @@ async function downloadHandler(
320353
}
321354
res.end();
322355
})
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);
325362
if (!res.headersSent) {
326363
res.statusCode = 400;
327364
}
@@ -330,24 +367,25 @@ async function downloadHandler(
330367
});
331368

332369
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
335371
const isClientError =
336372
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;
351389
res.end();
352390
});
353391
}

packages/open-next/src/utils/error.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface BaseOpenNextError {
33
readonly canIgnore: boolean;
44
// 0 - debug, 1 - warn, 2 - error
55
readonly logLevel: 0 | 1 | 2;
6+
readonly statusCode?: number;
67
}
78

89
// This is an error that can be totally ignored
@@ -11,9 +12,12 @@ export class IgnorableError extends Error implements BaseOpenNextError {
1112
readonly __openNextInternal = true;
1213
readonly canIgnore = true;
1314
readonly logLevel = 0;
14-
constructor(message: string) {
15+
readonly statusCode: number;
16+
17+
constructor(message: string, statusCode = 400) {
1518
super(message);
1619
this.name = "IgnorableError";
20+
this.statusCode = statusCode;
1721
}
1822
}
1923

@@ -23,9 +27,12 @@ export class RecoverableError extends Error implements BaseOpenNextError {
2327
readonly __openNextInternal = true;
2428
readonly canIgnore = true;
2529
readonly logLevel = 1;
26-
constructor(message: string) {
30+
readonly statusCode: number;
31+
32+
constructor(message: string, statusCode = 400) {
2733
super(message);
2834
this.name = "RecoverableError";
35+
this.statusCode = statusCode;
2936
}
3037
}
3138

@@ -34,9 +41,12 @@ export class FatalError extends Error implements BaseOpenNextError {
3441
readonly __openNextInternal = true;
3542
readonly canIgnore = false;
3643
readonly logLevel = 2;
37-
constructor(message: string) {
44+
readonly statusCode: number;
45+
46+
constructor(message: string, statusCode = 500) {
3847
super(message);
3948
this.name = "FatalError";
49+
this.statusCode = statusCode;
4050
}
4151
}
4252

0 commit comments

Comments
 (0)