Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 7545d88

Browse files
committed
fix: CusrorPaginator null encoding bug
the CursorPaginator encodes `None` values as the string "None", and on decoding it interprets that as just another string which is causing bugs when running the SQL, because we'll be comparing random types against a string instead of against null this fixes that by assigning a specific value (\x1f) that is unlikely to be in any user provided string that we would want to filter by
1 parent b8a000a commit 7545d88

File tree

2 files changed

+29
-2
lines changed

2 files changed

+29
-2
lines changed

graphql_api/helpers/connection.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from dataclasses import dataclass
33
from functools import cached_property
44
from typing import Any, Dict, List, Optional
5+
from base64 import b64decode
56

6-
from cursor_pagination import CursorPage, CursorPaginator
7+
from cursor_pagination import CursorPage, CursorPaginator, InvalidCursor
78
from django.db.models import QuerySet
89

910
from codecov.commands.exceptions import ValidationError
@@ -205,6 +206,14 @@ class DictCursorPaginator(CursorPaginator):
205206
if the dict access fails then it throws an exception, although it would be a different
206207
"""
207208

209+
def decode_cursor(self, cursor):
210+
try:
211+
orderings = b64decode(cursor.encode("ascii")).decode("utf8")
212+
orderings = orderings.split(self.delimiter)
213+
return [None if ordering == "\x1f" else ordering for ordering in orderings]
214+
except (TypeError, ValueError):
215+
raise InvalidCursor(self.invalid_cursor_message)
216+
208217
def position_from_instance(self, instance):
209218
position = []
210219
for order in self.ordering:
@@ -219,7 +228,7 @@ def position_from_instance(self, instance):
219228
except (KeyError, TypeError):
220229
raise attr_err from None
221230
parts.pop(0)
222-
position.append(str(attr))
231+
position.append("\x1f" if attr is None else str(attr))
223232
return position
224233

225234

graphql_api/helpers/tests/test_connection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,21 @@ def test_invalid_cursors(self):
155155

156156
with self.assertRaises(ValidationError):
157157
queryset_to_connection_sync(data, first=3, after="invalid")
158+
159+
def test_dict_cursor_paginator_null_encoding(self):
160+
from graphql_api.helpers.connection import DictCursorPaginator, field_order
161+
162+
repo_1 = RepositoryFactory(name="a", active=None)
163+
repo_2 = RepositoryFactory(name="b", active=True)
164+
repo_3 = RepositoryFactory(name="c", active=False)
165+
r = Repository.objects.all()
166+
167+
ordering = tuple(
168+
field_order(field, OrderingDirection.ASC) for field in ("active",)
169+
)
170+
171+
paginator = DictCursorPaginator(r, ordering=ordering)
172+
173+
assert paginator.position_from_instance(repo_1) == ["\x1f"]
174+
assert paginator.position_from_instance(repo_2) == ["True"]
175+
assert paginator.position_from_instance(repo_3) == ["False"]

0 commit comments

Comments
 (0)