From 73c1a9c7da22e4e093bd5ee28c7a432984134f4c Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Thu, 24 Sep 2026 04:22:41 +0000 Subject: [PATCH] Fix q=0 crash and ordering in get_choice_from_headers - A header entry with q=0 (e.g. "Accept-Language: fr;q=0, en") raised ZeroDivisionError from 1 / q_value. q=0 means "not acceptable", so such entries are now skipped; if nothing acceptable remains, None is returned as for a missing header. - With all=True the choices were returned in heap order rather than sorted order, e.g. "a;q=0.1,b;q=0.2,c;q=0.3,d;q=0.4,e;q=0.9" gave ['e', 'd', 'b', 'a', 'c']. Choices are now sorted by descending q, keeping header order for ties. - An unparsable q value such as "q=1.2.3" is skipped instead of raising. --- pygeoapi/util.py | 19 ++++++++++++------- tests/other/test_util.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pygeoapi/util.py b/pygeoapi/util.py index b60e187a08..f0962929ba 100644 --- a/pygeoapi/util.py +++ b/pygeoapi/util.py @@ -35,7 +35,6 @@ from datetime import date, datetime, time, timezone from decimal import Decimal from enum import Enum -from heapq import heappush import ipaddress import json import logging @@ -743,14 +742,20 @@ def get_choice_from_headers(headers: dict, match = re.match(r'^([^;]+)(?:;q=([\d.]+))?$', part.strip()) if match: value, q_value = match.groups() - q_value = float(q_value) if q_value else 1.0 + try: + q_value = float(q_value) if q_value else 1.0 + except ValueError: + continue - # Sort choices by q value and index - if 0 <= q_value <= 1: - heappush(choices, (1 / q_value, i, value)) + # q=0 means "not acceptable" (RFC 9110, section 12.4.2) + if 0 < q_value <= 1: + choices.append((-q_value, i, value)) - # Drop q value - sorted_choices = [choice[-1] for choice in choices] + if not choices: + return + + # Highest q value first; ties keep header order + sorted_choices = [choice[-1] for choice in sorted(choices)] # Return one or all choices return sorted_choices if all else sorted_choices[0] diff --git a/tests/other/test_util.py b/tests/other/test_util.py index 7cb321019d..5ede22b81b 100644 --- a/tests/other/test_util.py +++ b/tests/other/test_util.py @@ -330,6 +330,22 @@ def test_get_choice_from_headers(): assert util.get_choice_from_headers( {'accept-language': 'en_US', 'accept': '*/*'}, 'accept') == '*/*' + # all=True returns every choice ordered by q value, ties in header order + assert util.get_choice_from_headers( + {'accept-language': 'a;q=0.1,b;q=0.2,c;q=0.3,d;q=0.4,e;q=0.9,f'}, + 'accept-language', all=True) == ['f', 'e', 'd', 'c', 'b', 'a'] + assert util.get_choice_from_headers( + {'accept': 'text/html,application/json'}, + 'accept', all=True) == ['text/html', 'application/json'] + + # q=0 means "not acceptable" and must not raise ZeroDivisionError + assert util.get_choice_from_headers( + {'accept-language': 'fr;q=0,en'}, 'accept-language') == 'en' + assert util.get_choice_from_headers( + {'accept-language': 'fr;q=0'}, 'accept-language') is None + assert util.get_choice_from_headers( + {'accept-language': 'fr;q=0'}, 'accept-language', all=True) is None + @pytest.mark.parametrize('url,allow_internal,result', [ ['http://127.0.0.1/test', False, False],