Skip to content

fix: make is_package() read timeout configurable, default 30s#443

Merged
mandarons merged 2 commits into
mandarons:mainfrom
mikesoares:fix/is-package-read-timeout
May 20, 2026
Merged

fix: make is_package() read timeout configurable, default 30s#443
mandarons merged 2 commits into
mandarons:mainfrom
mikesoares:fix/is-package-read-timeout

Conversation

@mikesoares
Copy link
Copy Markdown
Contributor

Problem

is_package() in src/drive_file_existence.py calls item.open(stream=True) with no timeout. When Apple's CDN (cvws.icloud-content.com) accepts the TCP connection but never delivers the HTTP response, urllib3 blocks indefinitely (read timeout=None). The sync process freezes with no log output and no recovery path.

Observed in production: the sync process was stuck in a kernel read() syscall on an ESTABLISHED TCP connection to Apple's CDN for weeks, with zero log output and no way to self-recover without a container restart.

Root cause

The call chain passes **kwargs all the way through to session.get():

is_package() → item.open(stream=True)
             → DriveNode.open(**kwargs)
             → DriveService.get_file(**kwargs)
             → session.get(cdn_url, **kwargs)   ← no timeout

Because no timeout is passed, urllib3 uses None (infinite wait). A TCP connection that stays ESTABLISHED at the OS level but delivers no HTTP response data will block forever.

Fix

Adds a drive.request_timeout config option (default: 30s). The timeout is threaded through collect_file_for_download and process_file to is_package(), which passes it to item.open() via the existing **kwargs passthrough — no changes needed in icloudpy.

Users who want to adjust or disable the timeout can set request_timeout in their config.yaml:

drive:
  request_timeout: 60  # seconds; omit to use the default of 30

Changes

  • src/__init__.pyDEFAULT_REQUEST_TIMEOUT_SEC = 30
  • src/config_parser.pyget_drive_request_timeout() getter
  • src/drive_file_existence.pyis_package(item, timeout=DEFAULT_REQUEST_TIMEOUT_SEC)
  • src/drive_parallel_download.pycollect_file_for_download resolves and passes timeout
  • src/drive_sync_directory.py — threads config through _process_file_item
  • src/sync_drive.pyprocess_file resolves and passes timeout
  • config.yaml — documents the new option (commented out, showing default)

Tests

  • test_get_drive_request_timeout — config with value set
  • test_get_drive_request_timeout_default — config missing the key (returns default)
  • test_get_drive_request_timeout_none_configNone config (returns default)
  • test_is_package_read_timeoutReadTimeout is caught, returns False, open() called with timeout=30

Note: there are 20 pre-existing test failures in test_sync.py and test_usage.py in the local environment due to a missing /config/ path — these fail identically on the unmodified main branch and are unrelated to this change.

Without a read timeout, a stalled HTTPS connection to Apple's CDN
(cvws.icloud-content.com) would block the sync process indefinitely
with no recovery path (read timeout=None in urllib3).

Adds drive.request_timeout config option (default: 30s). The timeout
is threaded through collect_file_for_download and process_file to
is_package(), which passes it to item.open() via the existing **kwargs
passthrough in DriveNode.open() -> DriveService.get_file() -> session.get().
@mikesoares
Copy link
Copy Markdown
Contributor Author

Hi @mandarons — just wanted to flag a couple of things:

Tests pass. The build-and-push-docker CI failure is a fork-PR permissions issue (denied: installation not allowed to Write organization package) — the workflow tries to push to ghcr.io/mandarons/icloud-docker from a fork, which GitHub doesn't allow without explicit package write grants. It's not a code problem; the test and cache-requirements-install jobs both pass cleanly.

Production context. This fix came from a real incident — my sync container was stuck in a kernel read() syscall on an ESTABLISHED TCP connection to Apple's CDN (cvws.icloud-content.com) for ~8 weeks with zero log output and no self-recovery. The freeze is reproducible whenever Apple's CDN holds the TCP handshake open but never delivers HTTP response bytes.

Happy to adjust anything if you'd like changes before merging. Thanks for maintaining the project!

@mandarons mandarons requested a review from Copilot May 20, 2026 06:31
@mandarons mandarons merged commit 64fd7ee into mandarons:main May 20, 2026
6 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a configurable timeout for the iCloud Drive “package detection” request path to prevent indefinite hangs when probing items via is_package(), wiring the value from config through the drive sync call chain.

Changes:

  • Introduces drive.request_timeout with a default of 30 seconds (DEFAULT_REQUEST_TIMEOUT_SEC).
  • Adds config_parser.get_drive_request_timeout() and threads the resolved timeout into is_package(..., timeout=...).
  • Adds/updates tests and test config fixtures covering default/value behavior and ReadTimeout handling.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/__init__.py Adds the default request-timeout constant.
src/config_parser.py Adds a getter for drive.request_timeout.
src/drive_file_existence.py Applies the timeout to item.open() in is_package().
src/sync_drive.py Resolves timeout from config and passes it to is_package() in legacy process_file.
src/drive_sync_directory.py Threads config through file processing to reach timeout resolution.
src/drive_parallel_download.py Resolves timeout from config and passes it to is_package() during task collection.
config.yaml Documents the new drive.request_timeout option.
tests/data/test_config.yaml Adds request_timeout to the test fixture config.
tests/test_config_parser.py Adds tests for explicit/default/None-config timeout retrieval.
tests/test_sync_drive.py Adds test ensuring ReadTimeout is handled and timeout is passed to open().

Comment thread src/config_parser.py
Comment on lines +216 to +230
def get_drive_request_timeout(config: dict) -> int:
"""Return drive request timeout from config.

Args:
config: Configuration dictionary

Returns:
Request timeout in seconds (default: DEFAULT_REQUEST_TIMEOUT_SEC)
"""
config_path = ["drive", "request_timeout"]
return get_config_value_or_default(
config=config,
config_path=config_path,
default=DEFAULT_REQUEST_TIMEOUT_SEC,
)
Comment thread src/sync_drive.py
Comment on lines 92 to 96
files.add(local_file)
item_is_package = is_package(item=item)
timeout = config_parser.get_drive_request_timeout(config)
item_is_package = is_package(item=item, timeout=timeout)
if item_is_package:
if package_exists(item=item, local_package_path=local_file):
Comment on lines 64 to 70
# Thread-safe file set update
with files_lock:
files.add(local_file)

item_is_package = is_package(item=item)
timeout = config_parser.get_drive_request_timeout(config)
item_is_package = is_package(item=item, timeout=timeout)
if item_is_package:
Comment thread config.yaml
Comment on lines +60 to +61
# HTTP read timeout in seconds for iCloud API requests (default: 30)
# Prevents the sync process from hanging indefinitely on stalled connections
Comment thread tests/test_sync_drive.py
Comment on lines +715 to +725
"""Test is_package returns False on read timeout and passes timeout=30 to open()."""
import requests.exceptions

from src.drive_file_existence import is_package

with patch.object(self.file_item, "open") as mock_open:
mock_open.side_effect = requests.exceptions.ReadTimeout("Read timed out.")
result = is_package(self.file_item)
self.assertFalse(result)
mock_open.assert_called_once_with(stream=True, timeout=30)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants