Skip to content

Fix out-of-range when truncating a long exception message - #1929

Open
simonbeyer1 wants to merge 1 commit into
rstudio:mainfrom
simonbeyer1:fix/truncate-long-exception-message
Open

simonbeyer1 wants to merge 1 commit into
rstudio:mainfrom
simonbeyer1:fix/truncate-long-exception-message

Conversation

@simonbeyer1

@simonbeyer1 simonbeyer1 commented Sep 3, 2026 •

Copy link
Copy Markdown

Problem

conditionMessage_from_py_exception() in src/python.cpp truncates a Python
exception message that exceeds getOption("warning.length"): it keeps the first
two lines, inserts <...truncated...>, and appends the tail. The tail start is

int over(error.size() - max_msg_len);
...
std::string tail(error.substr(over + head.size() + trunc.size() + 20, std::string::npos));

which lands past the end of the string whenever the first two lines alone fill
the budget, that is whenever head.size() + trunc.size() + 20 > max_msg_len.
std::string::substr then throws std::out_of_range, Rcpp turns that into the
R error, and the Python message is lost:

reticulate::py_run_string("def boom(m): raise ValueError(m)")
reticulate::import_main()$boom(paste0(strrep("y", 1211), "\nsecond line"))
#> Error: basic_string::substr: __pos (which is 1555) > this->size() (which is 1282)

A SyntaxError reaches it reliably, because Python embeds the offending source
line in the message, so any expression longer than the budget turns its own
SyntaxError into an out_of_range:

reticulate::py_run_string("def compile_source(s): compile(s, '<string>', 'eval')")
reticulate::import_main()$compile_source(paste0("1 +* 2", strrep(" + 0", 1000)))
#> Error: basic_string::substr: __pos (which is 7194) > this->size() (which is 4120)

Both are on reticulate 1.46.0, and the code is unchanged on main.

We hit this through SymPy's parse_expr: a long equation made every parse
failure arrive in R as basic_string::substr instead of the reason, which took
a while to trace back to the message formatter.

Fix

Compute the budget once, drop the head when it does not fit, and derive the tail
start from error.size() so it cannot run past the end. The arithmetic is
otherwise unchanged: error.size() - (budget - head.size()) is the same
position as over + head.size() + trunc.size() + 20, only written so it cannot
exceed the string. Messages that already truncated correctly come out byte for
byte identical, checked against 1.46.0 for a two-line message, a single-line
message, a short message and one under the limit.

first_line_end_pos and second_line_start_pos also become std::size_t. As
int they held npos as -1, which happened to work through
substr(0, -1 + 1), but only by accident.

Test

tests/testthat/test-python-exceptions.R gains a case for both shapes. It fails
on the current code and passes with the fix.

conditionMessage_from_py_exception() keeps the first two lines of a long
message and appends the tail. The start of that tail was computed as
error.size() - max_msg_len + head.size() + trunc.size() + 20, which is past
the end of the string whenever the first two lines alone exceed
getOption("warning.length"). std::string::substr then throws, and the R error
becomes basic_string::substr: __pos > this->size() instead of the Python
message.

A SyntaxError hits this reliably, because Python embeds the offending source
line in the message: any expression longer than the message budget turns its
own SyntaxError into an out_of_range.

Clamp the head to the budget and derive the tail start from the string size.
Messages that already truncated correctly come out byte for byte the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant