Skip to content

pygeoapi does not respect header q values #1948

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
webb-ben opened this issue Feb 28, 2025 · 2 comments · May be fixed by #1952
Open

pygeoapi does not respect header q values #1948

webb-ben opened this issue Feb 28, 2025 · 2 comments · May be fixed by #1952
Assignees
Labels
bug Something isn't working

Comments

@webb-ben
Copy link
Member

Description
When sending a request to pygeoapi with multiple values in accept parameter, pygeoapi takes the first item instead of evaluating the q values of all options sent in the header.

Steps to Reproduce
Steps to reproduce the behavior:

  1. Send a request to pygeoapi with variable q values, and a lower q value as your first header:
    Accept: application/json;q=0.5,application/ld+json;q=1
  2. View response content format

Expected behavior
pygeoapi evaluates the full header context to decide the most appropriate header to send in the response.

Screenshots/Tracebacks
If applicable, add screenshots to help explain your problem.

Environment

  • OS: linux
  • Python version: 3.10/3.12
  • pygeoapi version: 0.20.dev0

Additional context
Add any other context about the problem here.

@webb-ben webb-ben added the bug Something isn't working label Feb 28, 2025
@webb-ben webb-ben self-assigned this Feb 28, 2025
@tomkralidis
Copy link
Member

Untested:

diff --git a/pygeoapi/api/__init__.py b/pygeoapi/api/__init__.py
index 7ce3ec4..854d667 100644
--- a/pygeoapi/api/__init__.py
+++ b/pygeoapi/api/__init__.py
@@ -339,7 +339,7 @@ class APIRequest:
         h = headers.get('accept', headers.get('Accept', '')).strip() # noqa
         (fmts, mimes) = zip(*FORMAT_TYPES.items())
         # basic support for complex types (i.e. with "q=0.x")
-        for type_ in (t.split(';')[0].strip() for t in h.split(',') if t):
+        for type_ in sorted(h.split(','), key=lambda x: x.split('q=')[-1], reverse=True):  # noqa
             if type_ in mimes:
                 idx_ = mimes.index(type_)
                 format_ = fmts[idx_]

@webb-ben
Copy link
Member Author

webb-ben commented Mar 1, 2025

Its worth noting we technically allow the type of preference setting for the following headers Accept, Content-Encoding, Accept-Language. I am in favor on consolidating the logic for extracting out the choice of header to a common util function to also address #1591

def get_choice_from_headers(headers: dict, 
                            header_name: str,
                            all: bool = False) -> Any:
    """
    Gets choices from a request dictionary,
    considering numerical ordering of preferences.
    Supported are complex preference strings (e.g. "fr-CH, fr;q=0.9, en;q=0.8")

    :param headers: `dict` of request headers.
    :param header_name: Name of request header.
    :param all: bool to return one or all header values.

    :returns: Sorted choices from header
    """
    try:
        # Clean headers and select header of interest
        cleaned_headers = {k.lower(): v for k, v in headers.items()}
        header = cleaned_headers[header_name.lower()].split(',')
    except:
        LOGGER.warning(f'Requested header not found: {header_name}')
        return

    # Parse choices, extracting optional q values (defaults to 1.0)
    choices = []
    for i, part in enumerate(header):
        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

            # Sort choices by q value and index
            if 0 <= q_value <= 1:
                heapq.heappush(choices, (1 / q_value, i, value))

    # Drop q value
    sorted_choices = [choice[-1] for choice in choices]

    # Return one or all choices
    return sorted_choices if all else sorted_choices[0]

@webb-ben webb-ben linked a pull request Mar 5, 2025 that will close this issue
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants