ioloop: reject zero/negative timedelta in PeriodicCallback constructor#3683
Draft
HrachShah wants to merge 4 commits into
Draft
ioloop: reject zero/negative timedelta in PeriodicCallback constructor#3683HrachShah wants to merge 4 commits into
HrachShah wants to merge 4 commits into
Conversation
added 4 commits
July 5, 2026 12:06
These two raise statements pass extra positional arguments to `ValueError` but use only the format string as the first argument, so the result is a tuple of (format_string, arg) instead of the formatted message. str(exc) then shows the literal format string rather than the intended text. Use string formatting to produce a single formatted message, matching the pattern used everywhere else in the codebase. Affected paths: - web.RequestHandler.xsrf_token: unknown xsrf cookie version %d - WebSocketProtocol13._process_server_headers: unsupported extension %r
bind_unix_socket raised ValueError('File %s exists and is not a socket', file)
and _HTTPConnection.run raised ValueError('unsupported auth_mode %s', mode).
Both pass a literal format string and the value as separate arguments, so
str(exc) renders the tuple repr with the format string and the value tucked
into e.args[1] rather than producing a usable error message. Replace with
the formatted single-argument form so the offending file path or mode name
is actually visible to a user reading the traceback or a log line.
Two regression tests:
- tornado/test/netutil_test.py::TestBindUnixSocket::
test_existing_non_socket_file_message_includes_path creates a regular file
where bind_unix_socket expects a socket, captures the leaked fd from the
error path, and asserts the path and 'exists and is not a socket' appear
in str(exc) with a single-arg ValueError.
- tornado/test/httpclient_test.py::HTTPClientCommonTestCase::
test_unsupported_auth_mode_message_format fetches /auth with auth_mode=
'asdf' against the simple client and asserts the 'asdf' literal appears in
the raised ValueError and '%s' does not.
stream_request_body and _has_stream_request_body used the two-argument form
raise TypeError("expected subclass of RequestHandler, got %r", cls)
which stores the format string and class as a tuple in e.args. str(e)
became the tuple repr
"('expected subclass of RequestHandler, got %r', <class '...'>)"
with the literal "%r" placeholder visible and the class tucked into
e.args[1]. The user-visible message did not mention RequestHandler or the
class, and any assertRaisesRegex over the formatted string failed.
Format the message before raising so str(e) is a single, formatted string
that names both the expected type and the offending class.
Add StreamRequestBodyTypeErrorTest with two regression guards that assert
the formatted message contains the class name, does not contain the
"%r" placeholder, and that e.args[0] is the same string as str(e). On
pre-fix code both fail with the literal "%r" placeholder visible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What do these changes do?
PeriodicCallbackvalidated that a numericcallback_timewas positive, but thedatetime.timedeltabranch only did the conversion and skipped the check. As aresult,
PeriodicCallback(lambda: None, datetime.timedelta(0))andPeriodicCallback(lambda: None, datetime.timedelta(microseconds=-1))wereaccepted, leaving a zero/negative
callback_timethat the scheduling loopwould either re-schedule immediately or silently break.
Restructure the constructor to always convert to milliseconds first, then
validate the resulting value is positive. The new code handles both branches
through a single
callback_time_msvariable.Are there changes in behavior for the user?
Yes, on the error path only. Pre-fix, malformed
timedelta(0)or negativetimedeltavalues produced aPeriodicCallbackwhosestart()would behaveunexpectedly. Post-fix, the constructor raises
ValueErrorwith the samemessage as the numeric branch.
No other callers in the tornado tree use these inputs, and the public signature
is unchanged.
Is it a substantial burden for the maintainers to support this?
No. The change is internal to one constructor; the public signature is
unchanged; the new error is a stricter version of the prior behavior.
Checklist
python3 -m pytest tornado/test/ioloop_test.py-> 56 passed)AssertionError: ValueError not raised; post-fix: passdocstring for the numeric branch)