Skip to content

Commit 20e0d93

Browse files
committed
refactor: use serialize-error for robust error handling in UrlContentFetcher
- Replace manual error type checking with serialize-error package - Simplifies error handling logic and ensures all error types are handled consistently - Remove unnecessary console.warn as serialize-error handles edge cases - Update tests to match new behavior
1 parent f481f6e commit 20e0d93

File tree

2 files changed

+52
-6
lines changed

2 files changed

+52
-6
lines changed

src/services/browser/UrlContentFetcher.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import TurndownService from "turndown"
77
// @ts-ignore
88
import PCR from "puppeteer-chromium-resolver"
99
import { fileExistsAtPath } from "../../utils/fs"
10+
import { serializeError } from "serialize-error"
1011

1112
// Timeout constants
1213
const URL_FETCH_TIMEOUT = 30_000 // 30 seconds
@@ -94,17 +95,18 @@ export class UrlContentFetcher {
9495
waitUntil: ["domcontentloaded", "networkidle2"],
9596
})
9697
} catch (error) {
97-
const errorMessage = error instanceof Error ? error.message : String(error)
98+
// Use serialize-error to safely extract error information
99+
const serializedError = serializeError(error)
100+
const errorMessage = serializedError.message || String(error)
101+
const errorName = serializedError.name
98102

99103
// Only retry for timeout or network-related errors
100104
const shouldRetry =
101105
errorMessage.includes("timeout") ||
102106
errorMessage.includes("net::") ||
103107
errorMessage.includes("NetworkError") ||
104108
errorMessage.includes("ERR_") ||
105-
(error instanceof Error && error.name === "TimeoutError") ||
106-
// Retry for unknown error types (when we can't determine the error)
107-
errorMessage === "[object Object]"
109+
errorName === "TimeoutError"
108110

109111
if (shouldRetry) {
110112
// If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback

src/services/browser/__tests__/UrlContentFetcher.spec.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ vi.mock("puppeteer-chromium-resolver", () => ({
6464
}),
6565
}))
6666

67+
// Mock serialize-error
68+
vi.mock("serialize-error", () => ({
69+
serializeError: vi.fn((error) => {
70+
if (error instanceof Error) {
71+
return { message: error.message, name: error.name }
72+
} else if (typeof error === "string") {
73+
return { message: error }
74+
} else if (error && typeof error === "object" && "message" in error) {
75+
return { message: String(error.message), name: "name" in error ? String(error.name) : undefined }
76+
} else {
77+
return { message: String(error) }
78+
}
79+
}),
80+
}))
81+
6782
describe("UrlContentFetcher", () => {
6883
let urlContentFetcher: UrlContentFetcher
6984
let mockContext: any
@@ -221,14 +236,43 @@ describe("UrlContentFetcher", () => {
221236

222237
it("should handle errors without message property", async () => {
223238
const errorWithoutMessage = { code: "UNKNOWN_ERROR" }
224-
mockPage.goto.mockRejectedValueOnce(errorWithoutMessage).mockResolvedValueOnce(undefined)
239+
mockPage.goto.mockRejectedValueOnce(errorWithoutMessage)
240+
241+
// serialize-error will convert this to a proper error with the object stringified
242+
await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow()
243+
244+
// Should not retry for non-network errors
245+
expect(mockPage.goto).toHaveBeenCalledTimes(1)
246+
})
247+
248+
it("should handle error objects with message property", async () => {
249+
const errorWithMessage = { message: "Custom error", code: "CUSTOM_ERROR" }
250+
mockPage.goto.mockRejectedValueOnce(errorWithMessage)
251+
252+
await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Custom error")
253+
254+
// Should not retry for error objects with message property (they're treated as known errors)
255+
expect(mockPage.goto).toHaveBeenCalledTimes(1)
256+
})
257+
258+
it("should retry for error objects with network-related messages", async () => {
259+
const errorWithNetworkMessage = { message: "net::ERR_CONNECTION_REFUSED", code: "NETWORK_ERROR" }
260+
mockPage.goto.mockRejectedValueOnce(errorWithNetworkMessage).mockResolvedValueOnce(undefined)
225261

226262
const result = await urlContentFetcher.urlToMarkdown("https://example.com")
227263

228-
// Should still retry since it can't determine the error type
264+
// Should retry for network-related errors even in non-Error objects
229265
expect(mockPage.goto).toHaveBeenCalledTimes(2)
230266
expect(result).toBe("# Test content")
231267
})
268+
269+
it("should handle string errors", async () => {
270+
const stringError = "Simple string error"
271+
mockPage.goto.mockRejectedValueOnce(stringError)
272+
273+
await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Simple string error")
274+
expect(mockPage.goto).toHaveBeenCalledTimes(1)
275+
})
232276
})
233277

234278
describe("closeBrowser", () => {

0 commit comments

Comments
 (0)