fix: close Chromium tabs on crawler fetch error paths
new_page() opened a tab that was only closed on the happy path; a `?` on wait_for_nav / content() leaked it (chromiumoxide doesn't close on drop), so errored fetches accumulated tabs in the shared browser. Add a generic, unit-tested `close_after(close, body)` helper and wrap the three fetch sites (chapter content, session probe, source navigate) so the tab is closed on every exit path — including navigate's content()-read branch, which previously leaked. Bump to 0.124.5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,28 @@ pub async fn wait_for_selector(
|
||||
/// under a second.
|
||||
pub const SELECTOR_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Run `body` to completion, then run `close` — on *every* exit path,
|
||||
/// including an early `?` / `return` inside `body`. chromiumoxide's `Page`
|
||||
/// does **not** close its CDP target on drop, so a fetch helper that opens a
|
||||
/// page with `browser.new_page(...)` and then bails via `?` on a nav /
|
||||
/// content-read error would leak a browser tab for the process's lifetime.
|
||||
/// Wrapping the fallible work in `close_after` — with `close` built from a
|
||||
/// clone of the page — guarantees the tab is closed regardless of how `body`
|
||||
/// returns.
|
||||
///
|
||||
/// Both arguments are pre-built futures, so this stays generic over the
|
||||
/// body's return type (the anyhow and `PageError` fetch paths both use it)
|
||||
/// and references no browser types — which also lets it be unit-tested
|
||||
/// without standing up a real `Page`.
|
||||
pub(crate) async fn close_after<R>(
|
||||
close: impl std::future::Future<Output = ()>,
|
||||
body: impl std::future::Future<Output = R>,
|
||||
) -> R {
|
||||
let result = body.await;
|
||||
close.await;
|
||||
result
|
||||
}
|
||||
|
||||
impl NavError {
|
||||
/// Does this navigation error indicate the underlying Chromium
|
||||
/// process has died or its CDP connection has dropped? Used by the
|
||||
@@ -150,6 +172,41 @@ mod tests {
|
||||
assert!(result.is_err(), "expected Elapsed on a hung future");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_after_runs_close_and_returns_value_on_ok() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
let closed = Arc::new(AtomicBool::new(false));
|
||||
let c = closed.clone();
|
||||
let out: anyhow::Result<i32> = close_after(
|
||||
async move { c.store(true, Ordering::SeqCst) },
|
||||
async { Ok(7) },
|
||||
)
|
||||
.await;
|
||||
assert_eq!(out.unwrap(), 7);
|
||||
assert!(closed.load(Ordering::SeqCst), "close must run on the happy path");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_after_runs_close_even_when_body_errs() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
let closed = Arc::new(AtomicBool::new(false));
|
||||
let c = closed.clone();
|
||||
// This is the leak the fix targets: the body bails before it could
|
||||
// close the page itself, and `close_after` must still close it.
|
||||
let out: anyhow::Result<()> =
|
||||
close_after(async move { c.store(true, Ordering::SeqCst) }, async {
|
||||
anyhow::bail!("nav failed")
|
||||
})
|
||||
.await;
|
||||
assert!(out.is_err());
|
||||
assert!(
|
||||
closed.load(Ordering::SeqCst),
|
||||
"the page must be closed even when the body returns Err"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nav_error_timeout_message_includes_duration() {
|
||||
let e = NavError::Timeout(Duration::from_secs(30));
|
||||
|
||||
Reference in New Issue
Block a user