# HG changeset patch # User John Rouillard # Date 1773869054 14400 # Node ID 1f8492d68aca876f95c453d757a81be4e381c542 # Parent 67ed90055e476f0f0a8016f2f1d363e13423d512 bug: using 'null' value for attributes causes error. In rest.py, filter out any attributes that are set to 'None'. GET on an endpoint can return 'null' values when the attribute is unset. E.G. for a user: { "address": "baddy@example.com", "alternate_addresses": null, "last_login": "2026-03-18.05:57:09", "organisation": null, "password": null, "phone": null, "queries": [], "realname": "Fred Jones", "roles": "User", "timezone": null, "username": "badeggs" } But this json can not be submitted to a PUT or POST endpoint. The validators for passwords, strings, integers etc. don't expect a None value. This change handles attributes with "null" (None) values in json objects by filtering them from the python object before processing. The null value can't be used to unset an attribute via PUT or POST. The 'remove' action using the PATCH verb can unset the value. Also there appears to be some missing checks in the back_anydbm and rdbms_common files for the password type. All the other types have a check: value is not None and not isinstance(.....) but passwords only have the 'not isinstance(....)' part. Not sure why this was the case. Looking at commit history didn't make me think it was intentional. diff -r 67ed90055e47 -r 1f8492d68aca CHANGES.txt --- a/CHANGES.txt Wed Mar 18 11:11:03 2026 -0400 +++ b/CHANGES.txt Wed Mar 18 17:24:14 2026 -0400 @@ -47,6 +47,13 @@ page is selected. (John Rouillard) - code cleanup replace bare except: with except Exception:. (patch by Sense_wang (haosenwang1018) applied by John Rouillard) +- handle "null" values in json objects sent to a rest endpoint by + filtering them from the object before processing. A "null" value + will not unset an attribute. The 'remove' action using the + PATCH verb can unset the value. Before this change "null" values + retrieved from the REST interface would cause errors when sent + using POST or PUT verbs. Also guard against password being set to + None. (John Rouillard) Features: diff -r 67ed90055e47 -r 1f8492d68aca roundup/backends/back_anydbm.py --- a/roundup/backends/back_anydbm.py Wed Mar 18 11:11:03 2026 -0400 +++ b/roundup/backends/back_anydbm.py Wed Mar 18 17:24:14 2026 -0400 @@ -984,7 +984,7 @@ (self.classname, newid, key), value) elif isinstance(prop, hyperdb.Password): - if not isinstance(value, password.Password): + if value is not None and not isinstance(value, password.Password): raise TypeError('new property "%s" not a Password' % key) elif isinstance(prop, hyperdb.Date): @@ -1344,7 +1344,7 @@ value) elif isinstance(prop, hyperdb.Password): - if not isinstance(value, password.Password): + if value is not None and not isinstance(value, password.Password): raise TypeError('new property "%s" not a ' 'Password' % propname) propvalues[propname] = value diff -r 67ed90055e47 -r 1f8492d68aca roundup/backends/rdbms_common.py --- a/roundup/backends/rdbms_common.py Wed Mar 18 11:11:03 2026 -0400 +++ b/roundup/backends/rdbms_common.py Wed Mar 18 17:24:14 2026 -0400 @@ -1818,7 +1818,7 @@ value) elif isinstance(prop, Password): - if not isinstance(value, password.Password): + if value is not None and not isinstance(value, password.Password): raise TypeError('new property "%s" not a Password' % key) elif isinstance(prop, Date): @@ -2132,7 +2132,7 @@ (self.classname, nodeid, propname), value) elif isinstance(prop, Password): - if not isinstance(value, password.Password): + if value is not None and not isinstance(value, password.Password): raise TypeError( 'new property "%s" not a Password' % propname) propvalues[propname] = value diff -r 67ed90055e47 -r 1f8492d68aca roundup/rest.py --- a/roundup/rest.py Wed Mar 18 11:11:03 2026 -0400 +++ b/roundup/rest.py Wed Mar 18 17:24:14 2026 -0400 @@ -34,6 +34,7 @@ from roundup.exceptions import Reject, UsageError from roundup.i18n import _ from roundup.rate_limit import Gcra, RateLimit +from roundup.timer import timer logger = logging.getLogger('roundup.rest') @@ -2432,6 +2433,8 @@ "acceptable": ", ".join(sorted( self.__accepted_content_type.keys()))})) + @timer(name="rest_dispatch", tag="args[2]", + writer=logging.getLogger('roundup.timer').error) def dispatch(self, method, uri, input_payload): """format and process the request""" output = None @@ -2747,6 +2750,8 @@ __slots__ = ("json_dict", "value") + @timer(name="simultateFieldStorage", tag="args[1][:10]", + writer=logging.getLogger('roundup.timer').error) def __init__(self, json_string): '''Parse the json string into an internal dict. @@ -2765,7 +2770,8 @@ self.json_dict = json.loads(json_string, parse_constant=raise_error_on_constant) self.value = [self.FsValue(index, self.json_dict[index]) - for index in self.json_dict] + for index in self.json_dict if + self.json_dict[index] is not None] except (JSONDecodeError, ValueError) as e: raise ValueError(e.args[0] + ". JSON is: " + json_string)