Skip to content

Fix an exception from the SAX input IO coming back as a TypeError - #476

Merged
ohler55 merged 1 commit into
ohler55:developfrom
ThomasSevestre:fix-sax-io-exception
Sep 20, 2026
Merged

ohler55 merged 1 commit into
ohler55:developfrom
ThomasSevestre:fix-sax-io-exception

Conversation

@ThomasSevestre

Copy link
Copy Markdown
Contributor

rescue_cb() is the rb_rescue handler for the read()/readpartial() calls Ox.sax_parse makes on an IO-like input. rb_rescue passes the handler the exception instance, but it handed that instance to rb_raise(), which expects a class, so Ruby raised a TypeError about the argument type while building the new exception and the error the IO raised was lost. A stream wrapper enforcing a size limit, or an IO failing mid-read, had to stash the error and re-raise it after sax_parse returned.

Re-raise the original error with the position appended to its message. Exception#exception clones the receiver and swaps the message in, so the class survives whatever initialize() it defines; rb_exc_new_str() would call .new(msg) and break on a zero-argument one.

TypeError was in the swallow list only because IO#read(len) returns nil at EOF and StringValuePtr() raises TypeError on nil, so a TypeError raised inside read() for any other reason ended the parse silently. io_cb() and partial_io_cb() now test NIL_P() and report end of input directly, and the handler no longer catches TypeError. EOFError still ends the parse, which is how readpartial() signals it.

Raising from the handler is safe: ox_sax_drive_cleanup() runs after the rb_protect() in sax_parse() catches it.

rescue_cb() is the rb_rescue handler for the read()/readpartial() calls
Ox.sax_parse makes on an IO-like input. rb_rescue passes the handler the
exception instance, but it handed that instance to rb_raise(), which
expects a class, so Ruby raised a TypeError about the argument type while
building the new exception and the error the IO raised was lost. A stream
wrapper enforcing a size limit, or an IO failing mid-read, had to stash
the error and re-raise it after sax_parse returned.

Re-raise the original error with the position appended to its message.
Exception#exception clones the receiver and swaps the message in, so the
class survives whatever initialize() it defines; rb_exc_new_str() would
call .new(msg) and break on a zero-argument one.

TypeError was in the swallow list only because IO#read(len) returns nil
at EOF and StringValuePtr() raises TypeError on nil, so a TypeError
raised inside read() for any other reason ended the parse silently.
io_cb() and partial_io_cb() now test NIL_P() and report end of input
directly, and the handler no longer catches TypeError. EOFError still
ends the parse, which is how readpartial() signals it.

Raising from the handler is safe: ox_sax_drive_cleanup() runs after the
rb_protect() in sax_parse() catches it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ohler55
ohler55 merged commit b8de2bd into ohler55:develop Sep 20, 2026
23 checks passed
@ohler55

ohler55 commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Thank you.

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.

2 participants