Skip to content

Commit da7200f

Browse files
committed
Keep duplicate diff scans on cached polling
1 parent d19b85d commit da7200f

4 files changed

Lines changed: 56 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
step when scans take several minutes to compare and network middleboxes
1414
(e.g. Azure NAT gateways, which default to a 4-minute TCP idle timeout)
1515
reap the idle connection (CE-354).
16+
- Duplicate scan pairs are resolved after an HTTP 409 and then polled through
17+
the same cached endpoint. This avoids automatically following the API's 302
18+
duplicate redirect with an uncached, potentially long-lived GET request.
1619
- The change is transparent: no flags or workflow changes are needed. If the
1720
org API token is missing the `diff-scans:create`, `diff-scans:list` or
1821
`full-scans:list` scopes — or the new flow fails for any other reason — the

socketsecurity/core/__init__.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,17 +1356,38 @@ def get_diff_scan_artifacts(
13561356
"before": head_full_scan_id,
13571357
"after": new_full_scan_id,
13581358
"description": f"Socket Security CLI v{__version__} scan comparison",
1359-
# A rerun against the same pair of scans returns the existing diff
1360-
# scan instead of failing with a 409.
1361-
"on_duplicate": "redirect",
13621359
}
1363-
result = self.sdk.diffscans.create_from_ids(self.config.org_slug, create_params)
1364-
diff_scan = result.get("diff_scan") or {}
1360+
try:
1361+
result = self.sdk.diffscans.create_from_ids(self.config.org_slug, create_params)
1362+
diff_scan = result.get("diff_scan") or {}
1363+
response_summary = result
1364+
except APIFailure as error:
1365+
if error.status_code != 409:
1366+
raise
1367+
1368+
# Do not use on_duplicate=redirect here. The SDK follows that 302
1369+
# automatically with a GET that lacks cached=true, which can leave
1370+
# the connection idle while an existing diff scan is still computing.
1371+
# Resolve the duplicate resource explicitly so every result fetch
1372+
# continues through the bounded cached polling path below.
1373+
existing = self.sdk.diffscans.list(
1374+
self.config.org_slug,
1375+
params={
1376+
"before_full_scan_id": head_full_scan_id,
1377+
"after_full_scan_id": new_full_scan_id,
1378+
"per_page": 1,
1379+
},
1380+
)
1381+
matches = existing.get("results") or []
1382+
diff_scan = matches[0] if matches else {}
1383+
response_summary = existing
1384+
13651385
diff_scan_id = diff_scan.get("id")
13661386
if not diff_scan_id:
1367-
raise Exception(f"Error creating diff scan: unexpected response: {str(result)[:500]}")
1368-
# An on_duplicate redirect can land on an already-computed diff scan, in
1369-
# which case the create response already carries the artifacts.
1387+
raise Exception(
1388+
"Error creating or resolving diff scan: "
1389+
f"unexpected response: {str(response_summary)[:500]}"
1390+
)
13701391
artifacts_dict = diff_scan.get("artifacts")
13711392

13721393
# cached=true is the polling contract (202 while computing, 200 when

tests/core/test_diff_scan_polling.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,32 @@ def test_poll_timeout_raises(core, no_sleep, monkeypatch):
7575
core.get_diff_scan_artifacts("head", "new")
7676

7777

78-
def test_duplicate_redirect_uses_embedded_artifacts(core, diff_scan_get_response):
79-
"""An on_duplicate redirect can return the computed diff scan straight away."""
80-
core.sdk.diffscans.create_from_ids.return_value = diff_scan_get_response
78+
def test_duplicate_conflict_uses_cached_polling(core, diff_scan_get_response):
79+
"""A duplicate is resolved explicitly so the SDK cannot follow an uncached redirect."""
80+
core.sdk.diffscans.create_from_ids.side_effect = APIFailure(
81+
"duplicate", status_code=409
82+
)
83+
core.sdk.diffscans.list.return_value = {
84+
"results": [{"id": "existing-diff-scan"}],
85+
}
8186

8287
artifacts = core.get_diff_scan_artifacts("head", "new")
8388

84-
core.sdk.diffscans.get.assert_not_called()
89+
create_params = core.sdk.diffscans.create_from_ids.call_args.args[1]
90+
assert "on_duplicate" not in create_params
91+
core.sdk.diffscans.list.assert_called_once_with(
92+
core.config.org_slug,
93+
params={
94+
"before_full_scan_id": "head",
95+
"after_full_scan_id": "new",
96+
"per_page": 1,
97+
},
98+
)
99+
core.sdk.diffscans.get.assert_called_once_with(
100+
core.config.org_slug,
101+
"existing-diff-scan",
102+
params={"cached": "true"},
103+
)
85104
assert len(artifacts.added) > 0
86105

87106

tests/core/test_sdk_methods.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def test_get_added_and_removed_packages(core):
234234
create_params = create_args[0][1]
235235
assert create_params["before"] == "head"
236236
assert create_params["after"] == "new"
237-
assert create_params["on_duplicate"] == "redirect"
237+
assert "on_duplicate" not in create_params
238238

239239
# cached=true is the polling contract (202 while computing, 200 when ready).
240240
# No omit_license_details param: the API ignores it for cached reads (cached

0 commit comments

Comments
 (0)