|
70 | 70 |
|
71 | 71 | PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html" |
72 | 72 | 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 |
73 | 108 |
|
74 | 109 |
|
75 | 110 | def _get_repo_version(path): |
@@ -319,12 +354,12 @@ def get_renderers(self): |
319 | 354 | """ |
320 | 355 | if self.action in ["list", "retrieve"]: |
321 | 356 | # 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", "") |
326 | 361 | renderers = [TemplateHTMLRenderer(), PyPISimpleHTMLRenderer()] |
327 | | - if PYPI_SIMPLE_V1_JSON in accept: |
| 362 | + if _accept_prefers_json(accept): |
328 | 363 | renderers.insert(0, PyPISimpleJSONRenderer()) |
329 | 364 | else: |
330 | 365 | renderers.append(PyPISimpleJSONRenderer()) |
|
0 commit comments