Skip to content

Commit d4de8e5

Browse files
Fail fast when pagination callbacks raise
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent bba779b commit d4de8e5

File tree

2 files changed

+145
-6
lines changed

2 files changed

+145
-6
lines changed

hyperbrowser/client/polling.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ def _ensure_non_awaitable(
9494
)
9595

9696

97+
def _invoke_non_retryable_callback(
98+
callback: Callable[..., T], *args: object, callback_name: str, operation_name: str
99+
) -> T:
100+
try:
101+
return callback(*args)
102+
except _NonRetryablePollingError:
103+
raise
104+
except Exception as exc:
105+
raise _NonRetryablePollingError(
106+
f"{callback_name} failed for {operation_name}: {exc}"
107+
) from exc
108+
109+
97110
def _validate_retry_config(
98111
*,
99112
max_attempts: int,
@@ -390,19 +403,34 @@ def collect_paginated_results(
390403
callback_name="get_next_page",
391404
operation_name=operation_name,
392405
)
393-
callback_result = on_page_success(page_response)
406+
callback_result = _invoke_non_retryable_callback(
407+
on_page_success,
408+
page_response,
409+
callback_name="on_page_success",
410+
operation_name=operation_name,
411+
)
394412
_ensure_non_awaitable(
395413
callback_result,
396414
callback_name="on_page_success",
397415
operation_name=operation_name,
398416
)
399-
current_page_batch = get_current_page_batch(page_response)
417+
current_page_batch = _invoke_non_retryable_callback(
418+
get_current_page_batch,
419+
page_response,
420+
callback_name="get_current_page_batch",
421+
operation_name=operation_name,
422+
)
400423
_ensure_non_awaitable(
401424
current_page_batch,
402425
callback_name="get_current_page_batch",
403426
operation_name=operation_name,
404427
)
405-
total_page_batches = get_total_page_batches(page_response)
428+
total_page_batches = _invoke_non_retryable_callback(
429+
get_total_page_batches,
430+
page_response,
431+
callback_name="get_total_page_batches",
432+
operation_name=operation_name,
433+
)
406434
_ensure_non_awaitable(
407435
total_page_batches,
408436
callback_name="get_total_page_batches",
@@ -480,19 +508,34 @@ async def collect_paginated_results_async(
480508
operation_name=operation_name,
481509
)
482510
page_response = await page_awaitable
483-
callback_result = on_page_success(page_response)
511+
callback_result = _invoke_non_retryable_callback(
512+
on_page_success,
513+
page_response,
514+
callback_name="on_page_success",
515+
operation_name=operation_name,
516+
)
484517
_ensure_non_awaitable(
485518
callback_result,
486519
callback_name="on_page_success",
487520
operation_name=operation_name,
488521
)
489-
current_page_batch = get_current_page_batch(page_response)
522+
current_page_batch = _invoke_non_retryable_callback(
523+
get_current_page_batch,
524+
page_response,
525+
callback_name="get_current_page_batch",
526+
operation_name=operation_name,
527+
)
490528
_ensure_non_awaitable(
491529
current_page_batch,
492530
callback_name="get_current_page_batch",
493531
operation_name=operation_name,
494532
)
495-
total_page_batches = get_total_page_batches(page_response)
533+
total_page_batches = _invoke_non_retryable_callback(
534+
get_total_page_batches,
535+
page_response,
536+
callback_name="get_total_page_batches",
537+
operation_name=operation_name,
538+
)
496539
_ensure_non_awaitable(
497540
total_page_batches,
498541
callback_name="get_total_page_batches",

tests/test_polling.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,50 @@ def on_page_success(response: dict) -> object:
390390
assert callback_attempts["count"] == 1
391391

392392

393+
def test_collect_paginated_results_fails_fast_when_on_page_success_raises():
394+
page_attempts = {"count": 0}
395+
396+
def get_next_page(page: int) -> dict:
397+
page_attempts["count"] += 1
398+
return {"current": 1, "total": 1, "items": []}
399+
400+
with pytest.raises(HyperbrowserError, match="on_page_success failed"):
401+
collect_paginated_results(
402+
operation_name="sync paginated callback exception",
403+
get_next_page=get_next_page,
404+
get_current_page_batch=lambda response: response["current"],
405+
get_total_page_batches=lambda response: response["total"],
406+
on_page_success=lambda response: (_ for _ in ()).throw(ValueError("boom")),
407+
max_wait_seconds=1.0,
408+
max_attempts=5,
409+
retry_delay_seconds=0.0001,
410+
)
411+
412+
assert page_attempts["count"] == 1
413+
414+
415+
def test_collect_paginated_results_fails_fast_when_page_batch_callback_raises():
416+
page_attempts = {"count": 0}
417+
418+
def get_next_page(page: int) -> dict:
419+
page_attempts["count"] += 1
420+
return {"current": 1, "total": 1, "items": []}
421+
422+
with pytest.raises(HyperbrowserError, match="get_current_page_batch failed"):
423+
collect_paginated_results(
424+
operation_name="sync paginated page-batch callback exception",
425+
get_next_page=get_next_page,
426+
get_current_page_batch=lambda response: response["missing"], # type: ignore[index]
427+
get_total_page_batches=lambda response: response["total"],
428+
on_page_success=lambda response: None,
429+
max_wait_seconds=1.0,
430+
max_attempts=5,
431+
retry_delay_seconds=0.0001,
432+
)
433+
434+
assert page_attempts["count"] == 1
435+
436+
393437
def test_collect_paginated_results_rejects_awaitable_current_page_callback_result():
394438
with pytest.raises(
395439
HyperbrowserError,
@@ -520,6 +564,58 @@ def on_page_success(response: dict) -> object:
520564
asyncio.run(run())
521565

522566

567+
def test_collect_paginated_results_async_fails_fast_when_on_page_success_raises():
568+
async def run() -> None:
569+
page_attempts = {"count": 0}
570+
571+
async def get_next_page(page: int) -> dict:
572+
page_attempts["count"] += 1
573+
return {"current": 1, "total": 1, "items": []}
574+
575+
with pytest.raises(HyperbrowserError, match="on_page_success failed"):
576+
await collect_paginated_results_async(
577+
operation_name="async paginated callback exception",
578+
get_next_page=get_next_page,
579+
get_current_page_batch=lambda response: response["current"],
580+
get_total_page_batches=lambda response: response["total"],
581+
on_page_success=lambda response: (_ for _ in ()).throw(
582+
ValueError("boom")
583+
),
584+
max_wait_seconds=1.0,
585+
max_attempts=5,
586+
retry_delay_seconds=0.0001,
587+
)
588+
589+
assert page_attempts["count"] == 1
590+
591+
asyncio.run(run())
592+
593+
594+
def test_collect_paginated_results_async_fails_fast_when_page_batch_callback_raises():
595+
async def run() -> None:
596+
page_attempts = {"count": 0}
597+
598+
async def get_next_page(page: int) -> dict:
599+
page_attempts["count"] += 1
600+
return {"current": 1, "total": 1, "items": []}
601+
602+
with pytest.raises(HyperbrowserError, match="get_total_page_batches failed"):
603+
await collect_paginated_results_async(
604+
operation_name="async paginated page-batch callback exception",
605+
get_next_page=get_next_page,
606+
get_current_page_batch=lambda response: response["current"],
607+
get_total_page_batches=lambda response: response["missing"], # type: ignore[index]
608+
on_page_success=lambda response: None,
609+
max_wait_seconds=1.0,
610+
max_attempts=5,
611+
retry_delay_seconds=0.0001,
612+
)
613+
614+
assert page_attempts["count"] == 1
615+
616+
asyncio.run(run())
617+
618+
523619
def test_collect_paginated_results_async_rejects_awaitable_current_page_callback_result():
524620
async def run() -> None:
525621
with pytest.raises(

0 commit comments

Comments
 (0)