Remediate the 2026-09-03 feature-test findings
This commit is contained in:
64
shopdb/utils/queryargs.py
Normal file
64
shopdb/utils/queryargs.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Reading a typed value out of the query string, without a 500.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
List endpoints filtered like this:
|
||||
|
||||
if type_id := request.args.get('typeid'):
|
||||
query = query.filter(Machine.machinetypeid == int(type_id))
|
||||
|
||||
`int('abc')` raises, nothing catches it, and the caller gets
|
||||
500 INTERNAL_ERROR with a traceback in the log. A wrong query argument is the
|
||||
caller's mistake and deserves a 400 saying which argument; a 500 says the server
|
||||
is broken and sends whoever is reading the log looking for a fault that is not
|
||||
there. The feature test run found the same shape in six places across five
|
||||
plugins, which makes it a property of how list routes are written rather than of
|
||||
any one route.
|
||||
|
||||
The helpers raise QueryArgError. Routes turn it into a 400 - or, more usually,
|
||||
nothing does anything at all, because the app-level handler in
|
||||
shopdb/__init__.py answers it. That is the point: a route should not have to
|
||||
remember.
|
||||
|
||||
Deliberately NOT the same policy as get_pagination_params, which falls back to
|
||||
a default for a bad page number. A nonsense page is harmless and the caller
|
||||
still wants their list; a nonsense FILTER is not, because silently ignoring it
|
||||
returns rows the caller believes they excluded.
|
||||
"""
|
||||
|
||||
from flask import request
|
||||
|
||||
|
||||
class QueryArgError(ValueError):
|
||||
"""A query argument was present but not of the type the route needs."""
|
||||
|
||||
def __init__(self, name, value, expected='a whole number'):
|
||||
self.name = name
|
||||
self.value = value
|
||||
super().__init__('%s must be %s (got %r)' % (name, expected, value))
|
||||
|
||||
|
||||
def intarg(name, default=None, request_obj=None):
|
||||
"""Read an integer query argument.
|
||||
|
||||
Absent or empty -> default. Present but not an integer -> QueryArgError,
|
||||
which the app answers as a 400 naming the argument.
|
||||
"""
|
||||
source = request_obj if request_obj is not None else request
|
||||
raw = source.args.get(name)
|
||||
if raw is None or raw == '':
|
||||
return default
|
||||
try:
|
||||
return int(raw)
|
||||
except (TypeError, ValueError):
|
||||
raise QueryArgError(name, raw)
|
||||
|
||||
|
||||
def intargs(*names, request_obj=None):
|
||||
"""Read several integer arguments at once; returns a dict of those present."""
|
||||
found = {}
|
||||
for name in names:
|
||||
value = intarg(name, request_obj=request_obj)
|
||||
if value is not None:
|
||||
found[name] = value
|
||||
return found
|
||||
Reference in New Issue
Block a user