Skip to content

Commit d1099e0

Browse files
Copilota-schur
andauthored
Fix cooldown bypass when PyPI JSON contains malformed version strings (#13412)
* Fix cooldown bug caused by malformed version strings in PyPI JSON response Co-authored-by: a-schur <227858738+a-schur@users.noreply.github.com> * Update test fixture with dynamic dates for cooldown validation Co-authored-by: a-schur <227858738+a-schur@users.noreply.github.com> * Use filtering with Version.correct? to validate versions instead of rescuing exceptions --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: a-schur <227858738+a-schur@users.noreply.github.com> Co-authored-by: a-schur <a-schur@github.com>
1 parent 0f5eabc commit d1099e0

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

python/lib/dependabot/python/package/package_details_fetcher.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ def format_version_releases(releases_json)
294294
.returns(T.nilable(Dependabot::Package::PackageRelease))
295295
end
296296
def format_version_release(version, release_data)
297+
# Skip versions that don't conform to PEP 440
298+
unless Dependabot::Python::Version.correct?(version)
299+
Dependabot.logger.warn("Skipping invalid version #{version}: does not match PEP 440")
300+
return nil
301+
end
302+
297303
upload_time = release_data["upload_time"]
298304
released_at = Time.parse(upload_time) if upload_time
299305
yanked = release_data["yanked"] || false
@@ -306,7 +312,7 @@ def format_version_release(version, release_data)
306312
requires_python: release_data["requires_python"]
307313
)
308314

309-
release = Dependabot::Package::PackageRelease.new(
315+
Dependabot::Package::PackageRelease.new(
310316
version: Dependabot::Python::Version.new(version),
311317
released_at: released_at,
312318
yanked: yanked,
@@ -316,7 +322,6 @@ def format_version_release(version, release_data)
316322
package_type: package_type,
317323
language: language
318324
)
319-
release
320325
end
321326

322327
sig do

python/spec/dependabot/python/package/package_details_fetcher_spec.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,43 @@
124124
end
125125
end
126126

127+
context "when JSON response contains a malformed version string" do
128+
let(:dependency_name) { "google-api-python-client" }
129+
let(:json_url) { "https://pypi.org/pypi/#{dependency_name}/json" }
130+
let(:registry_url) { "#{registry_base}/google-api-python-client/" }
131+
132+
before do
133+
stub_request(:get, json_url).to_return(
134+
status: 200,
135+
body: fixture("releases_api", "pypi", "pypi_json_response_with_malformed_version.json")
136+
)
137+
stub_request(:get, registry_url).to_return(
138+
status: 200,
139+
body: fixture("releases_api", "simple", "simple_index.html")
140+
)
141+
end
142+
143+
it "skips the malformed version but continues processing valid versions from JSON" do
144+
result = fetch
145+
146+
expect(result.releases).not_to be_empty
147+
expect(a_request(:get, json_url)).to have_been_made.once
148+
# Should NOT fall back to HTML since we can process valid versions from JSON
149+
expect(a_request(:get, registry_url)).not_to have_been_made
150+
151+
# Should have only the valid versions (2.184.0 and 2.185.0), not the malformed one
152+
version_strings = result.releases.map { |r| r.version.to_s }
153+
expect(version_strings).to include("2.184.0", "2.185.0")
154+
expect(version_strings).not_to include("1.0beta5prerelease")
155+
156+
# Verify that valid versions retain their upload_time (released_at)
157+
release_version = result.releases.find { |r| r.version.to_s == "2.185.0" }
158+
expect(release_version).not_to be_nil
159+
expect(release_version.released_at).not_to be_nil
160+
expect(release_version.released_at).to be_a(Time)
161+
end
162+
end
163+
127164
context "with an optional dependency postfix" do
128165
it "removes optional data from dependency name" do
129166
expect(fetcher.send(:remove_optional, "pyvista[io]")).to eq("pyvista")
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"info": {
3+
"name": "google-api-python-client",
4+
"summary": "Google API Client Library for Python",
5+
"author": "Google LLC",
6+
"license": "Apache 2.0"
7+
},
8+
"releases": {
9+
"1.0beta5prerelease": [
10+
{
11+
"filename": "google-api-python-client-1.0beta5prerelease.tar.gz",
12+
"version": "1.0beta5prerelease",
13+
"requires_python": null,
14+
"yanked": false,
15+
"upload_time": "2010-08-01T00:00:00",
16+
"url": "https://files.pythonhosted.org/packages/old/google-api-python-client-1.0beta5prerelease.tar.gz"
17+
}
18+
],
19+
"2.184.0": [
20+
{
21+
"filename": "google_api_python_client-2.184.0-py3-none-any.whl",
22+
"version": "2.184.0",
23+
"requires_python": ">=3.7",
24+
"yanked": false,
25+
"upload_time": "2025-10-18T14:54:31Z",
26+
"url": "https://files.pythonhosted.org/packages/test/google_api_python_client-2.184.0-py3-none-any.whl"
27+
}
28+
],
29+
"2.185.0": [
30+
{
31+
"filename": "google_api_python_client-2.185.0-py3-none-any.whl",
32+
"version": "2.185.0",
33+
"requires_python": ">=3.7",
34+
"yanked": false,
35+
"upload_time": "2025-10-27T14:54:31Z",
36+
"url": "https://files.pythonhosted.org/packages/test/google_api_python_client-2.185.0-py3-none-any.whl"
37+
}
38+
]
39+
}
40+
}

0 commit comments

Comments
 (0)