Skip to content

Commit 676a7b7

Browse files
committed
fix: improve handling of net::ERR_ABORTED errors in URL fetching
- Add specific retry logic for ERR_ABORTED errors with shorter timeout - Provide more descriptive error messages to users - Add translation for the new error message - Add tests for the new error handling behavior Fixes #6632
1 parent a88238f commit 676a7b7

File tree

4 files changed

+73
-19
lines changed

4 files changed

+73
-19
lines changed

src/core/mentions/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ function getUrlErrorMessage(error: unknown): string {
3535
if (errorMessage.includes("net::ERR_INTERNET_DISCONNECTED")) {
3636
return t("common:errors.no_internet")
3737
}
38+
if (errorMessage.includes("net::ERR_ABORTED")) {
39+
return t("common:errors.url_request_aborted")
40+
}
3841
if (errorMessage.includes("403") || errorMessage.includes("Forbidden")) {
3942
return t("common:errors.url_forbidden")
4043
}

src/i18n/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"no_internet": "No internet connection. Please check your network connection and try again.",
6262
"url_forbidden": "Access to this website is forbidden. The site may block automated access or require authentication.",
6363
"url_page_not_found": "The page was not found. Please check if the URL is correct.",
64+
"url_request_aborted": "The request to fetch the URL was aborted. This may happen if the site blocks automated access, requires authentication, or if there's a network issue. Please try again or check if the URL is accessible in a regular browser.",
6465
"url_fetch_failed": "Failed to fetch URL content: {{error}}",
6566
"url_fetch_error_with_url": "Error fetching content for {{url}}: {{error}}",
6667
"command_timeout": "Command execution timed out after {{seconds}} seconds",

src/services/browser/UrlContentFetcher.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -100,26 +100,46 @@ export class UrlContentFetcher {
100100
const errorMessage = serializedError.message || String(error)
101101
const errorName = serializedError.name
102102

103-
// Only retry for timeout or network-related errors
104-
const shouldRetry =
105-
errorMessage.includes("timeout") ||
106-
errorMessage.includes("net::") ||
107-
errorMessage.includes("NetworkError") ||
108-
errorMessage.includes("ERR_") ||
109-
errorName === "TimeoutError"
110-
111-
if (shouldRetry) {
112-
// If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback
113-
console.warn(
114-
`Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`,
115-
)
116-
await this.page.goto(url, {
117-
timeout: URL_FETCH_FALLBACK_TIMEOUT,
118-
waitUntil: ["domcontentloaded"],
119-
})
103+
// Special handling for ERR_ABORTED
104+
if (errorMessage.includes("net::ERR_ABORTED")) {
105+
console.error(`Navigation to ${url} was aborted: ${errorMessage}`)
106+
// For ERR_ABORTED, we'll try a more aggressive retry with just domcontentloaded
107+
// and a shorter timeout to quickly determine if the page is accessible
108+
try {
109+
await this.page.goto(url, {
110+
timeout: 10000, // 10 seconds for quick check
111+
waitUntil: ["domcontentloaded"],
112+
})
113+
} catch (retryError) {
114+
// If retry also fails, throw a more descriptive error
115+
const retrySerializedError = serializeError(retryError)
116+
const retryErrorMessage = retrySerializedError.message || String(retryError)
117+
throw new Error(
118+
`Failed to fetch URL content: ${retryErrorMessage}. The request was aborted, which may indicate the URL is inaccessible or blocked.`,
119+
)
120+
}
120121
} else {
121-
// For other errors, throw them as-is
122-
throw error
122+
// Only retry for timeout or network-related errors
123+
const shouldRetry =
124+
errorMessage.includes("timeout") ||
125+
errorMessage.includes("net::") ||
126+
errorMessage.includes("NetworkError") ||
127+
errorMessage.includes("ERR_") ||
128+
errorName === "TimeoutError"
129+
130+
if (shouldRetry) {
131+
// If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback
132+
console.warn(
133+
`Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`,
134+
)
135+
await this.page.goto(url, {
136+
timeout: URL_FETCH_FALLBACK_TIMEOUT,
137+
waitUntil: ["domcontentloaded"],
138+
})
139+
} else {
140+
// For other errors, throw them as-is
141+
throw error
142+
}
123143
}
124144
}
125145

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,36 @@ describe("UrlContentFetcher", () => {
273273
await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Simple string error")
274274
expect(mockPage.goto).toHaveBeenCalledTimes(1)
275275
})
276+
277+
it("should handle net::ERR_ABORTED with special retry logic", async () => {
278+
const abortedError = new Error("net::ERR_ABORTED at https://example.com")
279+
mockPage.goto.mockRejectedValueOnce(abortedError).mockResolvedValueOnce(undefined)
280+
281+
const result = await urlContentFetcher.urlToMarkdown("https://example.com")
282+
283+
expect(mockPage.goto).toHaveBeenCalledTimes(2)
284+
expect(mockPage.goto).toHaveBeenNthCalledWith(1, "https://example.com", {
285+
timeout: 30000,
286+
waitUntil: ["domcontentloaded", "networkidle2"],
287+
})
288+
expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", {
289+
timeout: 10000,
290+
waitUntil: ["domcontentloaded"],
291+
})
292+
expect(result).toBe("# Test content")
293+
})
294+
295+
it("should throw descriptive error when ERR_ABORTED retry also fails", async () => {
296+
const abortedError = new Error("net::ERR_ABORTED at https://example.com")
297+
const retryError = new Error("net::ERR_CONNECTION_REFUSED")
298+
mockPage.goto.mockRejectedValueOnce(abortedError).mockRejectedValueOnce(retryError)
299+
300+
await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow(
301+
"Failed to fetch URL content: net::ERR_CONNECTION_REFUSED. The request was aborted, which may indicate the URL is inaccessible or blocked.",
302+
)
303+
304+
expect(mockPage.goto).toHaveBeenCalledTimes(2)
305+
})
276306
})
277307

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

0 commit comments

Comments
 (0)