Skip to content

Commit 7562275

Browse files
committed
we default to json respond even when html needed
1 parent a47e20f commit 7562275

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

‎pulp_python/app/pypi/views.py‎

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,41 @@
7070

7171
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
7272
PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
73+
PYPI_TEXT_HTML = "text/html"
74+
75+
_SIMPLE_MEDIA_TYPES = (PYPI_SIMPLE_V1_JSON, PYPI_SIMPLE_V1_HTML, PYPI_TEXT_HTML)
76+
77+
78+
def _accept_prefers_json(accept_header):
79+
"""
80+
Determine whether an Accept header genuinely prefers the Simple JSON media type.
81+
82+
DRF's content negotiation matches renderers in renderer-list order and does not
83+
account for q-values among media types of equal specificity, so a client Accept
84+
header like "application/vnd.pypi.simple.v1+json, text/html;q=0.01" would
85+
otherwise lose to whichever HTML renderer happens to be listed first. We inspect
86+
the header ourselves and only prefer JSON when it has a strictly higher q-value
87+
than any of the HTML media types the client also advertises (ties favor whichever
88+
of the two was listed first, matching how pip/uv order their Accept headers).
89+
"""
90+
best_type, best_q = None, -1.0
91+
for token in accept_header.split(","):
92+
media_type, _, params = token.strip().partition(";")
93+
media_type = media_type.strip().lower()
94+
if media_type not in _SIMPLE_MEDIA_TYPES:
95+
continue
96+
q = 1.0
97+
for param in params.split(";"):
98+
param = param.strip()
99+
if param.startswith("q="):
100+
try:
101+
q = float(param[2:])
102+
except ValueError:
103+
q = 1.0
104+
break
105+
if q > best_q:
106+
best_q, best_type = q, media_type
107+
return best_type == PYPI_SIMPLE_V1_JSON
73108

74109

75110
def _get_repo_version(path):
@@ -319,12 +354,12 @@ def get_renderers(self):
319354
"""
320355
if self.action in ["list", "retrieve"]:
321356
# 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()
357+
# account for q-values. Put the PyPI JSON renderer first only when the
358+
# client's Accept header actually prefers it over HTML (as pip and uv do),
359+
# otherwise retain HTML as the default for browser and legacy clients.
360+
accept = self.request.META.get("HTTP_ACCEPT", "")
326361
renderers = [TemplateHTMLRenderer(), PyPISimpleHTMLRenderer()]
327-
if PYPI_SIMPLE_V1_JSON in accept:
362+
if _accept_prefers_json(accept):
328363
renderers.insert(0, PyPISimpleJSONRenderer())
329364
else:
330365
renderers.append(PyPISimpleJSONRenderer())

‎pulp_python/tests/functional/api/test_pypi_simple_api.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ def test_simple_json_detail_api(
171171
(PYPI_SIMPLE_V1_JSON, PYPI_SIMPLE_V1_JSON),
172172
# Clients such as pip and uv advertise JSON first, with HTML as a fallback.
173173
(f"{PYPI_SIMPLE_V1_JSON}, {PYPI_SIMPLE_V1_HTML}", PYPI_SIMPLE_V1_JSON),
174+
# pip/uv's real-world Accept header: JSON implicitly q=1, HTML variants
175+
# explicitly down-weighted as fallbacks.
176+
(
177+
f"{PYPI_SIMPLE_V1_JSON}, {PYPI_SIMPLE_V1_HTML};q=0.1, {PYPI_TEXT_HTML};q=0.01",
178+
PYPI_SIMPLE_V1_JSON,
179+
),
180+
# A client that explicitly weights HTML higher than JSON must get HTML,
181+
# even though JSON is technically present in the header.
182+
(f"{PYPI_SIMPLE_V1_HTML}, {PYPI_SIMPLE_V1_JSON};q=0.1", PYPI_SIMPLE_V1_HTML),
174183
# Everything else should be html
175184
("", PYPI_TEXT_HTML),
176185
("application/json", PYPI_TEXT_HTML),

0 commit comments

Comments
 (0)