From f995885d665491a5e86f82032a6dddf8c4a2884d Mon Sep 17 00:00:00 2001 From: Mateusz Sterczewski Date: Fri, 5 Jun 2026 07:33:54 +0200 Subject: [PATCH 1/5] CM-65436: retry scan notification on 404 after presigned upload After a successful S3 presigned POST, the scan service calls the file service to verify the upload exists. A transient 403 from the file service's S3 client is silently mapped to NotFound, causing the scan service to return 404. Add a targeted retry (up to 3 attempts, random exponential backoff) exclusively on the notify-server call so the scan proceeds without requiring a full re-upload. Co-Authored-By: Claude Sonnet 4.6 --- cycode/cli/apps/scan/code_scanner.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index 072e438e..ad516fa0 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -5,6 +5,7 @@ import requests import typer +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_random_exponential from cycode.cli import consts from cycode.cli.apps.scan.aggregation_report import try_set_aggregation_report_url_if_needed @@ -22,6 +23,7 @@ from cycode.cli.files_collector.path_documents import get_relevant_documents from cycode.cli.files_collector.sca.sca_file_collector import add_sca_dependencies_tree_documents_if_needed from cycode.cli.files_collector.zip_documents import zip_documents +from cycode.cli.exceptions.custom_exceptions import RequestHttpError from cycode.cli.models import CliError, Document, LocalScanResult from cycode.cli.utils.path_utils import get_absolute_path, get_path_by_os from cycode.cli.utils.progress_bar import ScanProgressBarSection @@ -32,7 +34,7 @@ set_issue_detected_by_scan_results, should_use_presigned_upload, ) -from cycode.cyclient.models import ZippedFileScanResult +from cycode.cyclient.models import ScanInitializationResponse, ZippedFileScanResult from cycode.logger import get_logger if TYPE_CHECKING: @@ -295,6 +297,26 @@ def scan_documents( print_local_scan_results(ctx, local_scan_results, errors) +@retry( + retry=retry_if_exception(lambda e: isinstance(e, RequestHttpError) and e.status_code == 404), + stop=stop_after_attempt(3), + wait=wait_random_exponential(multiplier=1, min=1, max=5), + reraise=True, +) +def _notify_scan_from_upload_id( + cycode_client: 'ScanClient', + scan_type: str, + upload_id: str, + zipped_documents: 'InMemoryZip', + scan_parameters: dict, + is_git_diff: bool, + is_commit_range: bool, +) -> 'ScanInitializationResponse': + return cycode_client.scan_repository_from_upload_id( + scan_type, upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range + ) + + def _perform_scan_v4_async( cycode_client: 'ScanClient', zipped_documents: 'InMemoryZip', @@ -312,8 +334,8 @@ def _perform_scan_v4_async( ) logger.debug('Uploaded zip to presigned URL') - scan_async_result = cycode_client.scan_repository_from_upload_id( - scan_type, upload_link.upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range + scan_async_result = _notify_scan_from_upload_id( + cycode_client, scan_type, upload_link.upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range ) logger.debug( 'Presigned upload scan request triggered, %s', From d62a1424e9c964ed877f0183494028d47817b34e Mon Sep 17 00:00:00 2001 From: Mateusz Sterczewski Date: Fri, 5 Jun 2026 07:36:43 +0200 Subject: [PATCH 2/5] style: ruff import sort Co-Authored-By: Claude Sonnet 4.6 --- cycode/cli/apps/scan/code_scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index ad516fa0..a3367425 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -19,11 +19,11 @@ ) from cycode.cli.config import configuration_manager from cycode.cli.exceptions import custom_exceptions +from cycode.cli.exceptions.custom_exceptions import RequestHttpError from cycode.cli.exceptions.handle_scan_errors import handle_scan_exception from cycode.cli.files_collector.path_documents import get_relevant_documents from cycode.cli.files_collector.sca.sca_file_collector import add_sca_dependencies_tree_documents_if_needed from cycode.cli.files_collector.zip_documents import zip_documents -from cycode.cli.exceptions.custom_exceptions import RequestHttpError from cycode.cli.models import CliError, Document, LocalScanResult from cycode.cli.utils.path_utils import get_absolute_path, get_path_by_os from cycode.cli.utils.progress_bar import ScanProgressBarSection From a8978ae62e8ab636acd92c0365bc5723c97a605a Mon Sep 17 00:00:00 2001 From: Mateusz Sterczewski Date: Fri, 5 Jun 2026 07:38:34 +0200 Subject: [PATCH 3/5] refactor: inline retry using Retrying context manager Eliminates the wrapper function that existed solely to carry the decorator. The Retrying context manager expresses the same intent inline without the extra indirection. Co-Authored-By: Claude Sonnet 4.6 --- cycode/cli/apps/scan/code_scanner.py | 37 +++++++++------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index a3367425..18a2bef6 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -5,7 +5,7 @@ import requests import typer -from tenacity import retry, retry_if_exception, stop_after_attempt, wait_random_exponential +from tenacity import Retrying, retry_if_exception, stop_after_attempt, wait_random_exponential from cycode.cli import consts from cycode.cli.apps.scan.aggregation_report import try_set_aggregation_report_url_if_needed @@ -34,7 +34,7 @@ set_issue_detected_by_scan_results, should_use_presigned_upload, ) -from cycode.cyclient.models import ScanInitializationResponse, ZippedFileScanResult +from cycode.cyclient.models import ZippedFileScanResult from cycode.logger import get_logger if TYPE_CHECKING: @@ -297,26 +297,6 @@ def scan_documents( print_local_scan_results(ctx, local_scan_results, errors) -@retry( - retry=retry_if_exception(lambda e: isinstance(e, RequestHttpError) and e.status_code == 404), - stop=stop_after_attempt(3), - wait=wait_random_exponential(multiplier=1, min=1, max=5), - reraise=True, -) -def _notify_scan_from_upload_id( - cycode_client: 'ScanClient', - scan_type: str, - upload_id: str, - zipped_documents: 'InMemoryZip', - scan_parameters: dict, - is_git_diff: bool, - is_commit_range: bool, -) -> 'ScanInitializationResponse': - return cycode_client.scan_repository_from_upload_id( - scan_type, upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range - ) - - def _perform_scan_v4_async( cycode_client: 'ScanClient', zipped_documents: 'InMemoryZip', @@ -334,9 +314,16 @@ def _perform_scan_v4_async( ) logger.debug('Uploaded zip to presigned URL') - scan_async_result = _notify_scan_from_upload_id( - cycode_client, scan_type, upload_link.upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range - ) + for attempt in Retrying( + retry=retry_if_exception(lambda e: isinstance(e, RequestHttpError) and e.status_code == 404), + stop=stop_after_attempt(3), + wait=wait_random_exponential(multiplier=1, min=1, max=5), + reraise=True, + ): + with attempt: + scan_async_result = cycode_client.scan_repository_from_upload_id( + scan_type, upload_link.upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range + ) logger.debug( 'Presigned upload scan request triggered, %s', {'scan_id': scan_async_result.scan_id, 'upload_id': upload_link.upload_id}, From c863d312270a4ebf1c8a7748fc51423b47494fa1 Mon Sep 17 00:00:00 2001 From: Mateusz Sterczewski Date: Fri, 5 Jun 2026 07:39:46 +0200 Subject: [PATCH 4/5] refactor: move upload-not-found retry to scan_client Retry on 404 belongs in the HTTP client layer alongside the other retry logic, not in the scan orchestration layer. Co-Authored-By: Claude Sonnet 4.6 --- cycode/cli/apps/scan/code_scanner.py | 15 +++------------ cycode/cyclient/scan_client.py | 7 +++++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index 18a2bef6..072e438e 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -5,7 +5,6 @@ import requests import typer -from tenacity import Retrying, retry_if_exception, stop_after_attempt, wait_random_exponential from cycode.cli import consts from cycode.cli.apps.scan.aggregation_report import try_set_aggregation_report_url_if_needed @@ -19,7 +18,6 @@ ) from cycode.cli.config import configuration_manager from cycode.cli.exceptions import custom_exceptions -from cycode.cli.exceptions.custom_exceptions import RequestHttpError from cycode.cli.exceptions.handle_scan_errors import handle_scan_exception from cycode.cli.files_collector.path_documents import get_relevant_documents from cycode.cli.files_collector.sca.sca_file_collector import add_sca_dependencies_tree_documents_if_needed @@ -314,16 +312,9 @@ def _perform_scan_v4_async( ) logger.debug('Uploaded zip to presigned URL') - for attempt in Retrying( - retry=retry_if_exception(lambda e: isinstance(e, RequestHttpError) and e.status_code == 404), - stop=stop_after_attempt(3), - wait=wait_random_exponential(multiplier=1, min=1, max=5), - reraise=True, - ): - with attempt: - scan_async_result = cycode_client.scan_repository_from_upload_id( - scan_type, upload_link.upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range - ) + scan_async_result = cycode_client.scan_repository_from_upload_id( + scan_type, upload_link.upload_id, zipped_documents, scan_parameters, is_git_diff, is_commit_range + ) logger.debug( 'Presigned upload scan request triggered, %s', {'scan_id': scan_async_result.scan_id, 'upload_id': upload_link.upload_id}, diff --git a/cycode/cyclient/scan_client.py b/cycode/cyclient/scan_client.py index 18f400ac..6ef9ba4b 100644 --- a/cycode/cyclient/scan_client.py +++ b/cycode/cyclient/scan_client.py @@ -5,6 +5,7 @@ import requests from requests import Response +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_random_exponential from cycode.cli import consts from cycode.cli.config import configuration_manager @@ -166,6 +167,12 @@ def upload_to_presigned_post( raise SlowUploadConnectionError from e raise + @retry( + retry=retry_if_exception(lambda e: isinstance(e, RequestHttpError) and e.status_code == 404), + stop=stop_after_attempt(3), + wait=wait_random_exponential(multiplier=1, min=1, max=5), + reraise=True, + ) def scan_repository_from_upload_id( self, scan_type: str, From bb3993da777348c87ea067d84d1ff65c595cd670 Mon Sep 17 00:00:00 2001 From: Mateusz Sterczewski Date: Fri, 5 Jun 2026 07:54:27 +0200 Subject: [PATCH 5/5] chore: add comment explaining retry placement Co-Authored-By: Claude Sonnet 4.6 --- cycode/cyclient/scan_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cycode/cyclient/scan_client.py b/cycode/cyclient/scan_client.py index 6ef9ba4b..283438c2 100644 --- a/cycode/cyclient/scan_client.py +++ b/cycode/cyclient/scan_client.py @@ -167,6 +167,8 @@ def upload_to_presigned_post( raise SlowUploadConnectionError from e raise + # Ideally this retry would live in _execute (CycodeClientBase) so all callers benefit, + # but that requires making the retry predicate configurable per-call — a larger refactor. @retry( retry=retry_if_exception(lambda e: isinstance(e, RequestHttpError) and e.status_code == 404), stop=stop_after_attempt(3),