From 676a7b7bd0e6d5b49ec82dbb7d1663b0169a278c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 3 Aug 2025 21:08:53 +0000 Subject: [PATCH 1/4] 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 --- src/core/mentions/index.ts | 3 + src/i18n/locales/en/common.json | 1 + src/services/browser/UrlContentFetcher.ts | 58 +++++++++++++------ .../__tests__/UrlContentFetcher.spec.ts | 30 ++++++++++ 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index ed3060859b2..a57dfcb6d4f 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -35,6 +35,9 @@ function getUrlErrorMessage(error: unknown): string { if (errorMessage.includes("net::ERR_INTERNET_DISCONNECTED")) { return t("common:errors.no_internet") } + if (errorMessage.includes("net::ERR_ABORTED")) { + return t("common:errors.url_request_aborted") + } if (errorMessage.includes("403") || errorMessage.includes("Forbidden")) { return t("common:errors.url_forbidden") } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index c8deee5cf4d..05d039a495d 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -61,6 +61,7 @@ "no_internet": "No internet connection. Please check your network connection and try again.", "url_forbidden": "Access to this website is forbidden. The site may block automated access or require authentication.", "url_page_not_found": "The page was not found. Please check if the URL is correct.", + "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.", "url_fetch_failed": "Failed to fetch URL content: {{error}}", "url_fetch_error_with_url": "Error fetching content for {{url}}: {{error}}", "command_timeout": "Command execution timed out after {{seconds}} seconds", diff --git a/src/services/browser/UrlContentFetcher.ts b/src/services/browser/UrlContentFetcher.ts index 8f4de7fbb40..5b9f5362f98 100644 --- a/src/services/browser/UrlContentFetcher.ts +++ b/src/services/browser/UrlContentFetcher.ts @@ -100,26 +100,46 @@ export class UrlContentFetcher { const errorMessage = serializedError.message || String(error) const errorName = serializedError.name - // Only retry for timeout or network-related errors - const shouldRetry = - errorMessage.includes("timeout") || - errorMessage.includes("net::") || - errorMessage.includes("NetworkError") || - errorMessage.includes("ERR_") || - errorName === "TimeoutError" - - if (shouldRetry) { - // If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback - console.warn( - `Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`, - ) - await this.page.goto(url, { - timeout: URL_FETCH_FALLBACK_TIMEOUT, - waitUntil: ["domcontentloaded"], - }) + // Special handling for ERR_ABORTED + if (errorMessage.includes("net::ERR_ABORTED")) { + console.error(`Navigation to ${url} was aborted: ${errorMessage}`) + // For ERR_ABORTED, we'll try a more aggressive retry with just domcontentloaded + // and a shorter timeout to quickly determine if the page is accessible + try { + await this.page.goto(url, { + timeout: 10000, // 10 seconds for quick check + waitUntil: ["domcontentloaded"], + }) + } catch (retryError) { + // If retry also fails, throw a more descriptive error + const retrySerializedError = serializeError(retryError) + const retryErrorMessage = retrySerializedError.message || String(retryError) + throw new Error( + `Failed to fetch URL content: ${retryErrorMessage}. The request was aborted, which may indicate the URL is inaccessible or blocked.`, + ) + } } else { - // For other errors, throw them as-is - throw error + // Only retry for timeout or network-related errors + const shouldRetry = + errorMessage.includes("timeout") || + errorMessage.includes("net::") || + errorMessage.includes("NetworkError") || + errorMessage.includes("ERR_") || + errorName === "TimeoutError" + + if (shouldRetry) { + // If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback + console.warn( + `Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`, + ) + await this.page.goto(url, { + timeout: URL_FETCH_FALLBACK_TIMEOUT, + waitUntil: ["domcontentloaded"], + }) + } else { + // For other errors, throw them as-is + throw error + } } } diff --git a/src/services/browser/__tests__/UrlContentFetcher.spec.ts b/src/services/browser/__tests__/UrlContentFetcher.spec.ts index 917b27c5f27..b619e40c311 100644 --- a/src/services/browser/__tests__/UrlContentFetcher.spec.ts +++ b/src/services/browser/__tests__/UrlContentFetcher.spec.ts @@ -273,6 +273,36 @@ describe("UrlContentFetcher", () => { await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Simple string error") expect(mockPage.goto).toHaveBeenCalledTimes(1) }) + + it("should handle net::ERR_ABORTED with special retry logic", async () => { + const abortedError = new Error("net::ERR_ABORTED at https://example.com") + mockPage.goto.mockRejectedValueOnce(abortedError).mockResolvedValueOnce(undefined) + + const result = await urlContentFetcher.urlToMarkdown("https://example.com") + + expect(mockPage.goto).toHaveBeenCalledTimes(2) + expect(mockPage.goto).toHaveBeenNthCalledWith(1, "https://example.com", { + timeout: 30000, + waitUntil: ["domcontentloaded", "networkidle2"], + }) + expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", { + timeout: 10000, + waitUntil: ["domcontentloaded"], + }) + expect(result).toBe("# Test content") + }) + + it("should throw descriptive error when ERR_ABORTED retry also fails", async () => { + const abortedError = new Error("net::ERR_ABORTED at https://example.com") + const retryError = new Error("net::ERR_CONNECTION_REFUSED") + mockPage.goto.mockRejectedValueOnce(abortedError).mockRejectedValueOnce(retryError) + + await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow( + "Failed to fetch URL content: net::ERR_CONNECTION_REFUSED. The request was aborted, which may indicate the URL is inaccessible or blocked.", + ) + + expect(mockPage.goto).toHaveBeenCalledTimes(2) + }) }) describe("closeBrowser", () => { From 9715fd0dab38ee2d86d788f514e7700fef90fc76 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 4 Aug 2025 20:40:29 +0000 Subject: [PATCH 2/4] feat: add url_request_aborted translations for all languages - Added translations for net::ERR_ABORTED error message in all 18 supported languages - Translations provide helpful context about possible causes (blocked access, authentication, network issues) - All translations validated with find-missing-translations.js script --- src/i18n/locales/ca/common.json | 1 + src/i18n/locales/de/common.json | 1 + src/i18n/locales/es/common.json | 1 + src/i18n/locales/fr/common.json | 1 + src/i18n/locales/hi/common.json | 1 + src/i18n/locales/id/common.json | 1 + src/i18n/locales/it/common.json | 1 + src/i18n/locales/ja/common.json | 1 + src/i18n/locales/ko/common.json | 1 + src/i18n/locales/nl/common.json | 1 + src/i18n/locales/pl/common.json | 1 + src/i18n/locales/pt-BR/common.json | 1 + src/i18n/locales/ru/common.json | 1 + src/i18n/locales/tr/common.json | 1 + src/i18n/locales/vi/common.json | 1 + src/i18n/locales/zh-CN/common.json | 1 + src/i18n/locales/zh-TW/common.json | 1 + 17 files changed, 17 insertions(+) diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index ad2af15efa6..d4fddfebf30 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -65,6 +65,7 @@ "no_internet": "No hi ha connexió a internet. Comprova la teva connexió de xarxa i torna-ho a provar.", "url_forbidden": "L'accés a aquest lloc web està prohibit. El lloc pot bloquejar l'accés automatitzat o requerir autenticació.", "url_page_not_found": "No s'ha trobat la pàgina. Comprova si la URL és correcta.", + "url_request_aborted": "La sol·licitud per obtenir la URL s'ha cancel·lat. Això pot passar si el lloc bloqueja l'accés automatitzat, requereix autenticació o si hi ha un problema de xarxa. Torna-ho a provar o comprova si la URL és accessible en un navegador normal.", "url_fetch_failed": "Error en obtenir el contingut de la URL: {{error}}", "url_fetch_error_with_url": "Error en obtenir contingut per {{url}}: {{error}}", "command_timeout": "L'execució de la comanda ha superat el temps d'espera de {{seconds}} segons", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 1dd8bd89e64..af69ed7cfe2 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -61,6 +61,7 @@ "no_internet": "Keine Internetverbindung. Bitte prüfe deine Netzwerkverbindung und versuche es erneut.", "url_forbidden": "Zugriff auf diese Website ist verboten. Die Seite könnte automatisierten Zugriff blockieren oder eine Authentifizierung erfordern.", "url_page_not_found": "Die Seite wurde nicht gefunden. Bitte prüfe, ob die URL korrekt ist.", + "url_request_aborted": "Die Anfrage zum Abrufen der URL wurde abgebrochen. Dies kann passieren, wenn die Seite automatisierten Zugriff blockiert, eine Authentifizierung erfordert oder wenn es ein Netzwerkproblem gibt. Bitte versuche es erneut oder prüfe, ob die URL in einem normalen Browser zugänglich ist.", "url_fetch_failed": "Fehler beim Abrufen des URL-Inhalts: {{error}}", "url_fetch_error_with_url": "Fehler beim Abrufen des Inhalts für {{url}}: {{error}}", "command_timeout": "Zeitüberschreitung bei der Befehlsausführung nach {{seconds}} Sekunden", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 47acd8e26a5..28e2fc38125 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -61,6 +61,7 @@ "no_internet": "Sin conexión a internet. Por favor verifica tu conexión de red e inténtalo de nuevo.", "url_forbidden": "El acceso a este sitio web está prohibido. El sitio puede bloquear el acceso automatizado o requerir autenticación.", "url_page_not_found": "La página no fue encontrada. Por favor verifica si la URL es correcta.", + "url_request_aborted": "La solicitud para obtener la URL fue cancelada. Esto puede ocurrir si el sitio bloquea el acceso automatizado, requiere autenticación o si hay un problema de red. Por favor intenta de nuevo o verifica si la URL es accesible en un navegador normal.", "url_fetch_failed": "Error al obtener el contenido de la URL: {{error}}", "url_fetch_error_with_url": "Error al obtener contenido para {{url}}: {{error}}", "command_timeout": "La ejecución del comando superó el tiempo de espera de {{seconds}} segundos", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 0103c8694e6..c3264e7ba3c 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -61,6 +61,7 @@ "no_internet": "Pas de connexion internet. Vérifie ta connexion réseau et réessaie.", "url_forbidden": "L'accès à ce site web est interdit. Le site peut bloquer l'accès automatisé ou nécessiter une authentification.", "url_page_not_found": "La page n'a pas été trouvée. Vérifie si l'URL est correcte.", + "url_request_aborted": "La demande de récupération de l'URL a été interrompue. Cela peut se produire si le site bloque l'accès automatisé, nécessite une authentification ou s'il y a un problème de réseau. Réessaie ou vérifie si l'URL est accessible dans un navigateur normal.", "url_fetch_failed": "Échec de récupération du contenu de l'URL : {{error}}", "url_fetch_error_with_url": "Erreur lors de la récupération du contenu pour {{url}} : {{error}}", "command_timeout": "L'exécution de la commande a expiré après {{seconds}} secondes", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index c18bf8fa7b3..c68d002e1d2 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -61,6 +61,7 @@ "no_internet": "इंटरनेट कनेक्शन नहीं है। कृपया अपना नेटवर्क कनेक्शन जांचें और फिर से कोशिश करें।", "url_forbidden": "इस वेबसाइट तक पहुंच प्रतिबंधित है। साइट स्वचालित पहुंच को ब्लॉक कर सकती है या प्रमाणीकरण की आवश्यकता हो सकती है।", "url_page_not_found": "पेज नहीं मिला। कृपया जांचें कि URL सही है।", + "url_request_aborted": "URL प्राप्त करने का अनुरोध रद्द कर दिया गया। यह तब हो सकता है जब साइट स्वचालित पहुंच को ब्लॉक करती है, प्रमाणीकरण की आवश्यकता होती है या नेटवर्क समस्या है। कृपया फिर से प्रयास करें या जांचें कि URL सामान्य ब्राउज़र में सुलभ है या नहीं।", "url_fetch_failed": "URL सामग्री प्राप्त करने में त्रुटि: {{error}}", "url_fetch_error_with_url": "{{url}} के लिए सामग्री प्राप्त करने में त्रुटि: {{error}}", "command_timeout": "कमांड निष्पादन {{seconds}} सेकंड के बाद समय समाप्त हो गया", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index eb36b9e8984..4045b7e8bb5 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -61,6 +61,7 @@ "no_internet": "Tidak ada koneksi internet. Silakan periksa koneksi jaringan kamu dan coba lagi.", "url_forbidden": "Akses ke situs web ini dilarang. Situs mungkin memblokir akses otomatis atau memerlukan autentikasi.", "url_page_not_found": "Halaman tidak ditemukan. Silakan periksa apakah URL sudah benar.", + "url_request_aborted": "Permintaan untuk mengambil URL dibatalkan. Ini bisa terjadi jika situs memblokir akses otomatis, memerlukan autentikasi, atau ada masalah jaringan. Silakan coba lagi atau periksa apakah URL dapat diakses di browser biasa.", "url_fetch_failed": "Gagal mengambil konten URL: {{error}}", "url_fetch_error_with_url": "Error mengambil konten untuk {{url}}: {{error}}", "command_timeout": "Eksekusi perintah waktu habis setelah {{seconds}} detik", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 9d0b36f03d2..a059c48a9bb 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -61,6 +61,7 @@ "no_internet": "Nessuna connessione internet. Verifica la tua connessione di rete e riprova.", "url_forbidden": "L'accesso a questo sito web è vietato. Il sito potrebbe bloccare l'accesso automatizzato o richiedere autenticazione.", "url_page_not_found": "La pagina non è stata trovata. Verifica se l'URL è corretto.", + "url_request_aborted": "La richiesta per recuperare l'URL è stata interrotta. Questo può accadere se il sito blocca l'accesso automatizzato, richiede autenticazione o se c'è un problema di rete. Riprova o verifica se l'URL è accessibile in un browser normale.", "url_fetch_failed": "Errore nel recupero del contenuto URL: {{error}}", "url_fetch_error_with_url": "Errore nel recupero del contenuto per {{url}}: {{error}}", "command_timeout": "Esecuzione del comando scaduta dopo {{seconds}} secondi", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 6451ed35339..f9eceb5979a 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -61,6 +61,7 @@ "no_internet": "インターネット接続がありません。ネットワーク接続を確認してもう一度試してください。", "url_forbidden": "このウェブサイトへのアクセスが禁止されています。サイトが自動アクセスをブロックしているか、認証が必要な可能性があります。", "url_page_not_found": "ページが見つかりませんでした。URLが正しいか確認してください。", + "url_request_aborted": "URLの取得リクエストが中断されました。これは、サイトが自動アクセスをブロックしている、認証が必要、またはネットワークの問題がある場合に発生する可能性があります。もう一度試すか、通常のブラウザでURLにアクセスできるか確認してください。", "url_fetch_failed": "URLコンテンツの取得に失敗しました:{{error}}", "url_fetch_error_with_url": "{{url}} のコンテンツ取得エラー:{{error}}", "command_timeout": "コマンドの実行が{{seconds}}秒後にタイムアウトしました", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index fc97d75dc58..4292677e30f 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -61,6 +61,7 @@ "no_internet": "인터넷 연결이 없습니다. 네트워크 연결을 확인하고 다시 시도해 주세요.", "url_forbidden": "이 웹사이트에 대한 접근이 금지되었습니다. 사이트가 자동 접근을 차단하거나 인증이 필요할 수 있습니다.", "url_page_not_found": "페이지를 찾을 수 없습니다. URL이 올바른지 확인해 주세요.", + "url_request_aborted": "URL을 가져오는 요청이 중단되었습니다. 사이트가 자동 접근을 차단하거나, 인증이 필요하거나, 네트워크 문제가 있을 때 발생할 수 있습니다. 다시 시도하거나 일반 브라우저에서 URL에 접근할 수 있는지 확인해 주세요.", "url_fetch_failed": "URL 콘텐츠 가져오기 실패: {{error}}", "url_fetch_error_with_url": "{{url}} 콘텐츠 가져오기 오류: {{error}}", "command_timeout": "명령 실행 시간이 {{seconds}}초 후 초과되었습니다", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index f722093b377..2133e7003b9 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -61,6 +61,7 @@ "no_internet": "Geen internetverbinding. Controleer je netwerkverbinding en probeer opnieuw.", "url_forbidden": "Toegang tot deze website is verboden. De site kan geautomatiseerde toegang blokkeren of authenticatie vereisen.", "url_page_not_found": "De pagina werd niet gevonden. Controleer of de URL correct is.", + "url_request_aborted": "Het verzoek om de URL op te halen is afgebroken. Dit kan gebeuren als de site geautomatiseerde toegang blokkeert, authenticatie vereist of als er een netwerkprobleem is. Probeer het opnieuw of controleer of de URL toegankelijk is in een normale browser.", "url_fetch_failed": "Fout bij ophalen van URL-inhoud: {{error}}", "url_fetch_error_with_url": "Fout bij ophalen van inhoud voor {{url}}: {{error}}", "command_timeout": "Time-out bij uitvoeren van commando na {{seconds}} seconden", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 8dea06033f4..1c63702911f 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -61,6 +61,7 @@ "no_internet": "Brak połączenia z internetem. Sprawdź połączenie sieciowe i spróbuj ponownie.", "url_forbidden": "Dostęp do tej strony internetowej jest zabroniony. Strona może blokować automatyczny dostęp lub wymagać uwierzytelnienia.", "url_page_not_found": "Strona nie została znaleziona. Sprawdź, czy URL jest poprawny.", + "url_request_aborted": "Żądanie pobrania URL zostało przerwane. Może to się zdarzyć, jeśli strona blokuje automatyczny dostęp, wymaga uwierzytelnienia lub występuje problem z siecią. Spróbuj ponownie lub sprawdź, czy URL jest dostępny w normalnej przeglądarce.", "url_fetch_failed": "Błąd pobierania zawartości URL: {{error}}", "url_fetch_error_with_url": "Błąd pobierania zawartości dla {{url}}: {{error}}", "command_timeout": "Przekroczono limit czasu wykonania polecenia po {{seconds}} sekundach", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index b0af270d4cd..6aeb5f6ee7e 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -65,6 +65,7 @@ "no_internet": "Sem conexão com a internet. Verifique sua conexão de rede e tente novamente.", "url_forbidden": "O acesso a este site está proibido. O site pode bloquear acesso automatizado ou exigir autenticação.", "url_page_not_found": "A página não foi encontrada. Verifique se a URL está correta.", + "url_request_aborted": "A solicitação para buscar a URL foi cancelada. Isso pode acontecer se o site bloqueia acesso automatizado, requer autenticação ou se há um problema de rede. Tente novamente ou verifique se a URL é acessível em um navegador normal.", "url_fetch_failed": "Falha ao buscar conteúdo da URL: {{error}}", "url_fetch_error_with_url": "Erro ao buscar conteúdo para {{url}}: {{error}}", "command_timeout": "A execução do comando excedeu o tempo limite após {{seconds}} segundos", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 716d42febc1..0d7b800d8b6 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -61,6 +61,7 @@ "no_internet": "Нет подключения к интернету. Проверь сетевое подключение и попробуй снова.", "url_forbidden": "Доступ к этому веб-сайту запрещен. Сайт может блокировать автоматический доступ или требовать аутентификацию.", "url_page_not_found": "Страница не найдена. Проверь правильность URL.", + "url_request_aborted": "Запрос на получение URL был прерван. Это может произойти, если сайт блокирует автоматический доступ, требует аутентификацию или есть проблемы с сетью. Попробуй снова или проверь, доступен ли URL в обычном браузере.", "url_fetch_failed": "Ошибка получения содержимого URL: {{error}}", "url_fetch_error_with_url": "Ошибка получения содержимого для {{url}}: {{error}}", "command_timeout": "Время выполнения команды истекло через {{seconds}} секунд", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 18324d723f8..49d0fba1139 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -61,6 +61,7 @@ "no_internet": "İnternet bağlantısı yok. Ağ bağlantını kontrol et ve tekrar dene.", "url_forbidden": "Bu web sitesine erişim yasak. Site otomatik erişimi engelliyor veya kimlik doğrulama gerektiriyor olabilir.", "url_page_not_found": "Sayfa bulunamadı. URL'nin doğru olup olmadığını kontrol et.", + "url_request_aborted": "URL'yi getirme isteği iptal edildi. Bu, sitenin otomatik erişimi engellemesi, kimlik doğrulama gerektirmesi veya bir ağ sorunu olması durumunda gerçekleşebilir. Tekrar dene veya URL'nin normal bir tarayıcıda erişilebilir olup olmadığını kontrol et.", "url_fetch_failed": "URL içeriği getirme hatası: {{error}}", "url_fetch_error_with_url": "{{url}} için içerik getirme hatası: {{error}}", "command_timeout": "Komut çalıştırma {{seconds}} saniye sonra zaman aşımına uğradı", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 772371555e7..3f8947e6204 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -61,6 +61,7 @@ "no_internet": "Không có kết nối internet. Vui lòng kiểm tra kết nối mạng và thử lại.", "url_forbidden": "Truy cập vào trang web này bị cấm. Trang có thể chặn truy cập tự động hoặc yêu cầu xác thực.", "url_page_not_found": "Không tìm thấy trang. Vui lòng kiểm tra URL có đúng không.", + "url_request_aborted": "Yêu cầu lấy URL đã bị hủy. Điều này có thể xảy ra nếu trang web chặn truy cập tự động, yêu cầu xác thực hoặc có vấn đề về mạng. Vui lòng thử lại hoặc kiểm tra xem URL có thể truy cập được trong trình duyệt thông thường không.", "url_fetch_failed": "Lỗi lấy nội dung URL: {{error}}", "url_fetch_error_with_url": "Lỗi lấy nội dung cho {{url}}: {{error}}", "command_timeout": "Thực thi lệnh đã hết thời gian chờ sau {{seconds}} giây", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index c06ce9d9fd7..808d5345725 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -66,6 +66,7 @@ "no_internet": "无网络连接。请检查网络连接并重试。", "url_forbidden": "访问此网站被禁止。该网站可能阻止自动访问或需要身份验证。", "url_page_not_found": "页面未找到。请检查 URL 是否正确。", + "url_request_aborted": "获取 URL 的请求被中止。如果网站阻止自动访问、需要身份验证或存在网络问题,就可能发生这种情况。请重试或检查 URL 是否可以在普通浏览器中访问。", "url_fetch_failed": "获取 URL 内容失败:{{error}}", "url_fetch_error_with_url": "获取 {{url}} 内容时出错:{{error}}", "command_timeout": "命令执行超时,{{seconds}} 秒后", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index f443ef97775..81e098fbcf0 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -61,6 +61,7 @@ "no_internet": "無網路連線。請檢查網路連線並重試。", "url_forbidden": "存取此網站被禁止。該網站可能封鎖自動存取或需要身分驗證。", "url_page_not_found": "找不到頁面。請檢查 URL 是否正確。", + "url_request_aborted": "取得 URL 的請求被中止。如果網站封鎖自動存取、需要身分驗證或存在網路問題,就可能發生這種情況。請重試或檢查 URL 是否可以在一般瀏覽器中存取。", "url_fetch_failed": "取得 URL 內容失敗:{{error}}", "url_fetch_error_with_url": "取得 {{url}} 內容時發生錯誤:{{error}}", "command_timeout": "命令執行超時,{{seconds}} 秒後", From d71860f72db169e491541175ef0a7bec588cf81b Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 4 Aug 2025 22:20:43 +0000 Subject: [PATCH 3/4] refactor: address review comments for ERR_ABORTED handling - Extract retry logic into reusable helper method retryNavigation() - Add URL_FETCH_ABORTED_RETRY_TIMEOUT constant (10 seconds) - Change console.error to console.warn for consistency - Preserve specific error types in retry error messages - Add test case for successful retry after ERR_ABORTED --- src/services/browser/UrlContentFetcher.ts | 41 ++++++++++++------- .../__tests__/UrlContentFetcher.spec.ts | 28 ++++++++++++- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/services/browser/UrlContentFetcher.ts b/src/services/browser/UrlContentFetcher.ts index 5b9f5362f98..9c10a090bc4 100644 --- a/src/services/browser/UrlContentFetcher.ts +++ b/src/services/browser/UrlContentFetcher.ts @@ -12,6 +12,7 @@ import { serializeError } from "serialize-error" // Timeout constants const URL_FETCH_TIMEOUT = 30_000 // 30 seconds const URL_FETCH_FALLBACK_TIMEOUT = 20_000 // 20 seconds for fallback +const URL_FETCH_ABORTED_RETRY_TIMEOUT = 10_000 // 10 seconds for ERR_ABORTED retry interface PCRStats { puppeteer: { launch: typeof launch } @@ -79,6 +80,20 @@ export class UrlContentFetcher { this.page = undefined } + /** + * Helper method to retry navigation with different parameters + */ + private async retryNavigation( + url: string, + timeout: number, + waitUntil: ("load" | "domcontentloaded" | "networkidle0" | "networkidle2")[], + ): Promise { + if (!this.page) { + throw new Error("Page not initialized") + } + await this.page.goto(url, { timeout, waitUntil }) + } + // must make sure to call launchBrowser before and closeBrowser after using this async urlToMarkdown(url: string): Promise { if (!this.browser || !this.page) { @@ -90,10 +105,7 @@ export class UrlContentFetcher { this should be sufficient for most doc sites */ try { - await this.page.goto(url, { - timeout: URL_FETCH_TIMEOUT, - waitUntil: ["domcontentloaded", "networkidle2"], - }) + await this.retryNavigation(url, URL_FETCH_TIMEOUT, ["domcontentloaded", "networkidle2"]) } catch (error) { // Use serialize-error to safely extract error information const serializedError = serializeError(error) @@ -102,20 +114,22 @@ export class UrlContentFetcher { // Special handling for ERR_ABORTED if (errorMessage.includes("net::ERR_ABORTED")) { - console.error(`Navigation to ${url} was aborted: ${errorMessage}`) + console.warn(`Navigation to ${url} was aborted: ${errorMessage}`) // For ERR_ABORTED, we'll try a more aggressive retry with just domcontentloaded // and a shorter timeout to quickly determine if the page is accessible try { - await this.page.goto(url, { - timeout: 10000, // 10 seconds for quick check - waitUntil: ["domcontentloaded"], - }) + await this.retryNavigation(url, URL_FETCH_ABORTED_RETRY_TIMEOUT, ["domcontentloaded"]) } catch (retryError) { - // If retry also fails, throw a more descriptive error + // If retry also fails, throw a more descriptive error while preserving the specific error type const retrySerializedError = serializeError(retryError) const retryErrorMessage = retrySerializedError.message || String(retryError) + + // Extract the specific error type (e.g., ERR_CONNECTION_REFUSED, ERR_TIMEOUT) + const errorTypeMatch = retryErrorMessage.match(/net::ERR_\w+|ERR_\w+/) + const errorType = errorTypeMatch ? errorTypeMatch[0] : "Unknown error" + throw new Error( - `Failed to fetch URL content: ${retryErrorMessage}. The request was aborted, which may indicate the URL is inaccessible or blocked.`, + `Failed to fetch URL content: ${errorType} - ${retryErrorMessage}. The request was aborted, which may indicate the URL is inaccessible or blocked.`, ) } } else { @@ -132,10 +146,7 @@ export class UrlContentFetcher { console.warn( `Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`, ) - await this.page.goto(url, { - timeout: URL_FETCH_FALLBACK_TIMEOUT, - waitUntil: ["domcontentloaded"], - }) + await this.retryNavigation(url, URL_FETCH_FALLBACK_TIMEOUT, ["domcontentloaded"]) } else { // For other errors, throw them as-is throw error diff --git a/src/services/browser/__tests__/UrlContentFetcher.spec.ts b/src/services/browser/__tests__/UrlContentFetcher.spec.ts index b619e40c311..215cfe623d3 100644 --- a/src/services/browser/__tests__/UrlContentFetcher.spec.ts +++ b/src/services/browser/__tests__/UrlContentFetcher.spec.ts @@ -298,11 +298,37 @@ describe("UrlContentFetcher", () => { mockPage.goto.mockRejectedValueOnce(abortedError).mockRejectedValueOnce(retryError) await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow( - "Failed to fetch URL content: net::ERR_CONNECTION_REFUSED. The request was aborted, which may indicate the URL is inaccessible or blocked.", + "Failed to fetch URL content: net::ERR_CONNECTION_REFUSED - net::ERR_CONNECTION_REFUSED. The request was aborted, which may indicate the URL is inaccessible or blocked.", ) expect(mockPage.goto).toHaveBeenCalledTimes(2) }) + + it("should succeed when ERR_ABORTED retry is successful", async () => { + const abortedError = new Error("net::ERR_ABORTED at https://example.com") + // First call fails with ERR_ABORTED, second call (retry) succeeds + mockPage.goto.mockRejectedValueOnce(abortedError).mockResolvedValueOnce(undefined) + + const result = await urlContentFetcher.urlToMarkdown("https://example.com") + + // Should have called goto twice + expect(mockPage.goto).toHaveBeenCalledTimes(2) + + // First call with full wait conditions + expect(mockPage.goto).toHaveBeenNthCalledWith(1, "https://example.com", { + timeout: 30000, + waitUntil: ["domcontentloaded", "networkidle2"], + }) + + // Second call (retry) with reduced timeout and simpler wait condition + expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", { + timeout: 10000, + waitUntil: ["domcontentloaded"], + }) + + // Should return the markdown content successfully + expect(result).toBe("# Test content") + }) }) describe("closeBrowser", () => { From ecc9286e644cba25dec2e76e561f2d47f29a87df Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Tue, 5 Aug 2025 13:24:58 -0500 Subject: [PATCH 4/4] refactor: simplify ERR_ABORTED handling to use standard retry logic - Remove special aggressive retry logic for ERR_ABORTED errors - Remove URL_FETCH_ABORTED_RETRY_TIMEOUT constant - Remove retryNavigation helper method - Treat ERR_ABORTED like other network errors with standard retry - Keep user-friendly translation for better error messaging - Update tests to reflect simplified error handling --- src/services/browser/UrlContentFetcher.ts | 77 ++++++------------- .../__tests__/UrlContentFetcher.spec.ts | 34 +------- 2 files changed, 27 insertions(+), 84 deletions(-) diff --git a/src/services/browser/UrlContentFetcher.ts b/src/services/browser/UrlContentFetcher.ts index 9c10a090bc4..8f4de7fbb40 100644 --- a/src/services/browser/UrlContentFetcher.ts +++ b/src/services/browser/UrlContentFetcher.ts @@ -12,7 +12,6 @@ import { serializeError } from "serialize-error" // Timeout constants const URL_FETCH_TIMEOUT = 30_000 // 30 seconds const URL_FETCH_FALLBACK_TIMEOUT = 20_000 // 20 seconds for fallback -const URL_FETCH_ABORTED_RETRY_TIMEOUT = 10_000 // 10 seconds for ERR_ABORTED retry interface PCRStats { puppeteer: { launch: typeof launch } @@ -80,20 +79,6 @@ export class UrlContentFetcher { this.page = undefined } - /** - * Helper method to retry navigation with different parameters - */ - private async retryNavigation( - url: string, - timeout: number, - waitUntil: ("load" | "domcontentloaded" | "networkidle0" | "networkidle2")[], - ): Promise { - if (!this.page) { - throw new Error("Page not initialized") - } - await this.page.goto(url, { timeout, waitUntil }) - } - // must make sure to call launchBrowser before and closeBrowser after using this async urlToMarkdown(url: string): Promise { if (!this.browser || !this.page) { @@ -105,52 +90,36 @@ export class UrlContentFetcher { this should be sufficient for most doc sites */ try { - await this.retryNavigation(url, URL_FETCH_TIMEOUT, ["domcontentloaded", "networkidle2"]) + await this.page.goto(url, { + timeout: URL_FETCH_TIMEOUT, + waitUntil: ["domcontentloaded", "networkidle2"], + }) } catch (error) { // Use serialize-error to safely extract error information const serializedError = serializeError(error) const errorMessage = serializedError.message || String(error) const errorName = serializedError.name - // Special handling for ERR_ABORTED - if (errorMessage.includes("net::ERR_ABORTED")) { - console.warn(`Navigation to ${url} was aborted: ${errorMessage}`) - // For ERR_ABORTED, we'll try a more aggressive retry with just domcontentloaded - // and a shorter timeout to quickly determine if the page is accessible - try { - await this.retryNavigation(url, URL_FETCH_ABORTED_RETRY_TIMEOUT, ["domcontentloaded"]) - } catch (retryError) { - // If retry also fails, throw a more descriptive error while preserving the specific error type - const retrySerializedError = serializeError(retryError) - const retryErrorMessage = retrySerializedError.message || String(retryError) - - // Extract the specific error type (e.g., ERR_CONNECTION_REFUSED, ERR_TIMEOUT) - const errorTypeMatch = retryErrorMessage.match(/net::ERR_\w+|ERR_\w+/) - const errorType = errorTypeMatch ? errorTypeMatch[0] : "Unknown error" - - throw new Error( - `Failed to fetch URL content: ${errorType} - ${retryErrorMessage}. The request was aborted, which may indicate the URL is inaccessible or blocked.`, - ) - } + // Only retry for timeout or network-related errors + const shouldRetry = + errorMessage.includes("timeout") || + errorMessage.includes("net::") || + errorMessage.includes("NetworkError") || + errorMessage.includes("ERR_") || + errorName === "TimeoutError" + + if (shouldRetry) { + // If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback + console.warn( + `Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`, + ) + await this.page.goto(url, { + timeout: URL_FETCH_FALLBACK_TIMEOUT, + waitUntil: ["domcontentloaded"], + }) } else { - // Only retry for timeout or network-related errors - const shouldRetry = - errorMessage.includes("timeout") || - errorMessage.includes("net::") || - errorMessage.includes("NetworkError") || - errorMessage.includes("ERR_") || - errorName === "TimeoutError" - - if (shouldRetry) { - // If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback - console.warn( - `Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`, - ) - await this.retryNavigation(url, URL_FETCH_FALLBACK_TIMEOUT, ["domcontentloaded"]) - } else { - // For other errors, throw them as-is - throw error - } + // For other errors, throw them as-is + throw error } } diff --git a/src/services/browser/__tests__/UrlContentFetcher.spec.ts b/src/services/browser/__tests__/UrlContentFetcher.spec.ts index 215cfe623d3..132d73a409e 100644 --- a/src/services/browser/__tests__/UrlContentFetcher.spec.ts +++ b/src/services/browser/__tests__/UrlContentFetcher.spec.ts @@ -274,7 +274,7 @@ describe("UrlContentFetcher", () => { expect(mockPage.goto).toHaveBeenCalledTimes(1) }) - it("should handle net::ERR_ABORTED with special retry logic", async () => { + it("should retry net::ERR_ABORTED like other network errors", async () => { const abortedError = new Error("net::ERR_ABORTED at https://example.com") mockPage.goto.mockRejectedValueOnce(abortedError).mockResolvedValueOnce(undefined) @@ -286,49 +286,23 @@ describe("UrlContentFetcher", () => { waitUntil: ["domcontentloaded", "networkidle2"], }) expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", { - timeout: 10000, + timeout: 20000, waitUntil: ["domcontentloaded"], }) expect(result).toBe("# Test content") }) - it("should throw descriptive error when ERR_ABORTED retry also fails", async () => { + it("should throw error when ERR_ABORTED retry also fails", async () => { const abortedError = new Error("net::ERR_ABORTED at https://example.com") const retryError = new Error("net::ERR_CONNECTION_REFUSED") mockPage.goto.mockRejectedValueOnce(abortedError).mockRejectedValueOnce(retryError) await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow( - "Failed to fetch URL content: net::ERR_CONNECTION_REFUSED - net::ERR_CONNECTION_REFUSED. The request was aborted, which may indicate the URL is inaccessible or blocked.", + "net::ERR_CONNECTION_REFUSED", ) expect(mockPage.goto).toHaveBeenCalledTimes(2) }) - - it("should succeed when ERR_ABORTED retry is successful", async () => { - const abortedError = new Error("net::ERR_ABORTED at https://example.com") - // First call fails with ERR_ABORTED, second call (retry) succeeds - mockPage.goto.mockRejectedValueOnce(abortedError).mockResolvedValueOnce(undefined) - - const result = await urlContentFetcher.urlToMarkdown("https://example.com") - - // Should have called goto twice - expect(mockPage.goto).toHaveBeenCalledTimes(2) - - // First call with full wait conditions - expect(mockPage.goto).toHaveBeenNthCalledWith(1, "https://example.com", { - timeout: 30000, - waitUntil: ["domcontentloaded", "networkidle2"], - }) - - // Second call (retry) with reduced timeout and simpler wait condition - expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", { - timeout: 10000, - waitUntil: ["domcontentloaded"], - }) - - // Should return the markdown content successfully - expect(result).toBe("# Test content") - }) }) describe("closeBrowser", () => {