Skip to content

Commit 2653a46

Browse files
committed
Prefer simple JSON when possible
fixes: #1370 Generated-by: gpt-5.6-luna
1 parent f5c965b commit 2653a46

4 files changed

Lines changed: 23 additions & 12 deletions

File tree

CHANGES/1370.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prefer the PyPI Simple API JSON response when clients such as pip and uv advertise JSON alongside HTML.

pulp_python/app/pypi/views.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,17 @@ def get_renderers(self):
318318
Uses custom renderers for PyPI Simple API endpoints, defaulting to standard ones.
319319
"""
320320
if self.action in ["list", "retrieve"]:
321-
# Ordered by priority if multiple content types are present
322-
return [TemplateHTMLRenderer(), PyPISimpleHTMLRenderer(), PyPISimpleJSONRenderer()]
321+
# DRF resolves equally-specific media types in renderer order and does not
322+
# account for q-values. Put the PyPI JSON renderer first when the client
323+
# explicitly advertises it (as pip and uv do), otherwise retain HTML as the
324+
# default for browser and legacy clients.
325+
accept = self.request.META.get("HTTP_ACCEPT", "").lower()
326+
renderers = [TemplateHTMLRenderer(), PyPISimpleHTMLRenderer()]
327+
if PYPI_SIMPLE_V1_JSON in accept:
328+
renderers.insert(0, PyPISimpleJSONRenderer())
329+
else:
330+
renderers.append(PyPISimpleJSONRenderer())
331+
return renderers
323332
else:
324333
return [JSONRenderer(), BrowsableAPIRenderer()]
325334

pulp_python/tests/functional/api/test_pypi_simple_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ def test_simple_json_detail_api(
169169
(PYPI_TEXT_HTML, PYPI_TEXT_HTML),
170170
(PYPI_SIMPLE_V1_HTML, PYPI_SIMPLE_V1_HTML),
171171
(PYPI_SIMPLE_V1_JSON, PYPI_SIMPLE_V1_JSON),
172-
# Follows defined ordering (html, pypi html, pypi json)
173-
(f"{PYPI_SIMPLE_V1_JSON}, {PYPI_SIMPLE_V1_HTML}", PYPI_SIMPLE_V1_HTML),
172+
# Clients such as pip and uv advertise JSON first, with HTML as a fallback.
173+
(f"{PYPI_SIMPLE_V1_JSON}, {PYPI_SIMPLE_V1_HTML}", PYPI_SIMPLE_V1_JSON),
174174
# Everything else should be html
175175
("", PYPI_TEXT_HTML),
176176
("application/json", PYPI_TEXT_HTML),
@@ -191,3 +191,5 @@ def test_simple_api_content_headers(
191191
response = requests.get(url, headers={"Accept": header})
192192
assert response.status_code == 200
193193
assert result in response.headers["Content-Type"]
194+
if url == detail_url and result == PYPI_SIMPLE_V1_JSON:
195+
assert all(file["upload-time"] for file in response.json()["files"])

pulp_python/tests/functional/api/test_simple_cache.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,13 @@ def test_simple_cache_separate_accept_headers(synced_distro):
9191

9292

9393
@pytest.mark.parallel
94-
def test_simple_cache_format_json_does_not_poison_html(synced_distro):
94+
def test_simple_cache_negotiated_media_types_are_separate(synced_distro):
9595
"""
96-
A ?format=json response must not poison a later request with the same Accept.
96+
JSON and HTML responses must not poison each other in the cache.
9797
98-
Clients like uv/pip send an Accept that allows both JSON and HTML. DRF's
99-
?format=json overrides negotiation to JSON, while the same Accept without
100-
that query param selects HTML. Caching must key on the negotiated type so
101-
the JSON entry is not served (and re-rendered) for the HTML request.
98+
Clients like uv/pip send an Accept that allows both JSON and HTML. The
99+
negotiated JSON response must be cached separately from an explicit HTML
100+
response.
102101
"""
103102
url = f"{urljoin(synced_distro.base_url, 'simple/')}aiohttp"
104103
# pip/uv-style Accept: JSON preferred, HTML still acceptable
@@ -112,13 +111,13 @@ def test_simple_cache_format_json_does_not_poison_html(synced_distro):
112111
assert r_json.headers["X-PULP-CACHE"] == "MISS"
113112
assert r_json.json()["name"] == "aiohttp"
114113

115-
r_html = requests.get(url, headers=headers)
114+
r_html = requests.get(url, headers={"Accept": PYPI_TEXT_HTML})
116115
assert r_html.status_code == 200
117116
assert PYPI_TEXT_HTML in r_html.headers["Content-Type"]
118117
assert r_html.headers["X-PULP-CACHE"] == "MISS"
119118
assert b"<a href=" in r_html.content
120119

121-
r_html_hit = requests.get(url, headers=headers)
120+
r_html_hit = requests.get(url, headers={"Accept": PYPI_TEXT_HTML})
122121
assert r_html_hit.status_code == 200
123122
assert r_html_hit.headers["X-PULP-CACHE"] == "HIT"
124123
assert PYPI_TEXT_HTML in r_html_hit.headers["Content-Type"]

0 commit comments

Comments
 (0)