Skip to content

Split socket/SSLSocket nonblock methods into exception: overloads#2963

Open
zonuexe wants to merge 3 commits into
ruby:masterfrom
zonuexe:fix/nonblock-exception-overloads
Open

Split socket/SSLSocket nonblock methods into exception: overloads#2963
zonuexe wants to merge 3 commits into
ruby:masterfrom
zonuexe:fix/nonblock-exception-overloads

Conversation

@zonuexe
Copy link
Copy Markdown
Contributor

@zonuexe zonuexe commented May 21, 2026

Summary

The non-blocking socket methods (and OpenSSL SSLSocket equivalents) take an exception: keyword that selects the return value: the normal result when exception: true (the default), or a :wait_readable / :wait_writable symbol when exception: false. The signatures expressed this with a single ?exception: boolish parameter and a union return type (or plain untyped), so the symbol leaked into the result type even when exception: was not passed.

This splits each method into true / false literal overloads so the return type reflects the argument — the same treatment applied to StringScanner#scan_full / #search_full in #2959.

Ruby Method Change C function
BasicSocket#recv_nonblock Split exception: into true/false overloads (false… | :wait_readable) rsock_s_recvfrom_nonblock
BasicSocket#recvmsg_nonblock Split exception: into true/false overloads (false… | :wait_readable) rsock_bsock_recvmsg_nonblock
BasicSocket#sendmsg_nonblock Split exception: into true/false overloads (false… | :wait_writable) rsock_bsock_sendmsg_nonblock
OpenSSL::SSL::SSLSocket#sysread_nonblock Replace (*untyped) -> untyped with true/false overloads (false… | :wait_readable | :wait_writable | nil) ossl_ssl_read_nonblock
OpenSSL::SSL::SSLSocket#syswrite_nonblock Replace (*untyped) -> untyped with true/false overloads (false… | :wait_readable | :wait_writable | nil) ossl_ssl_write_nonblock
Socket#accept_nonblock Split exception: into true/false overloads (false… | :wait_readable) rsock_s_accept_nonblock
Socket#connect_nonblock Split exception: into true/false overloads (false… | :wait_writable) sock_connect_nonblock
Socket#recvfrom_nonblock Split exception: into true/false overloads (false… | :wait_readable) rsock_s_recvfrom_nonblock
TCPServer#accept_nonblock Split exception: into true/false overloads (false… | :wait_readable) rsock_s_accept_nonblock
UDPSocket#recvfrom_nonblock Split exception: into true/false overloads (false… | :wait_readable) rsock_s_recvfrom_nonblock
UNIXServer#accept_nonblock Split exception: into true/false overloads (false… | :wait_readable) rsock_s_accept_nonblock

Out of scope

  • The OpenSSL read_nonblock, write_nonblock, connect_nonblock, and accept_nonblock methods already use true/false overloads — no change needed.
  • read_nonblock / write_nonblock are not redefined under stdlib/socket; they inherit the core IO signatures, which already have the overloads.
  • PTY.check(pid, raise) is a related parameter-driven return type (raise: true can only return nil), but it belongs to a different method family rather than non-blocking I/O; left for a separate change.

Test plan

  • bundle exec rbs -r socket validate
  • bundle exec rbs -r openssl validate
  • bundle exec rubocop stdlib/socket/0/ stdlib/openssl/0/openssl.rbs

zonuexe added 2 commits May 21, 2026 15:37
The `exception:` keyword of the non-blocking socket methods selects the
return value: the normal result when `exception: true` (the default),
or a `:wait_readable` / `:wait_writable` symbol when `exception: false`.
Express this with `true`/`false` literal overloads instead of a single
`?exception: boolish` parameter with a union return type.
…erloads

`SSLSocket#sysread_nonblock` and `#syswrite_nonblock` were typed as
`(*untyped) -> untyped`. Like the other non-blocking methods, their
`exception:` keyword selects the return value: the normal result when
`exception: true` (the default), or a `:wait_readable` / `:wait_writable`
symbol (or `nil`) when `exception: false`. Express this with `true`/`false`
literal overloads.
@zonuexe zonuexe changed the title Split socket nonblock methods into exception: overloads Split socket/SSLSocket nonblock methods into exception: overloads May 21, 2026
Comment thread stdlib/openssl/0/openssl.rbs Outdated
Comment on lines +9345 to +9346
def syswrite_nonblock: (_ToS string, ?exception: true) -> Integer
| (_ToS string, exception: false) -> (Integer | :wait_readable | :wait_writable | nil)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _ToS interface applies to all objects except BasicObject (e.g., nil, Hash, etc.). However, the actual implementation uses StringValue, and objects like nil should be excluded. I think string is appropriate.

And, Based on the implementation, syswrite_nonblock raises an error even when exception: false is specified. This is asymmetric to sysread_nonblock.
It seems there is no possibility of it returning nil.

Comment thread stdlib/socket/0/basic_socket.rbs Outdated
#
def recv_nonblock: (Integer maxlen, ?Integer flags, ?String buf, ?exception: boolish) -> (String | :wait_readable)
def recv_nonblock: (Integer maxlen, ?Integer flags, ?String buf, ?exception: true) -> String
| (Integer maxlen, ?Integer flags, ?String buf, exception: false) -> (String | :wait_readable)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the documentation states that Socket#recv_nonblock returns nil, there is a possibility that nil may be returned; the same may be true for recvmsg_nonblock and Socket#recvfrom_nonblock.

Per review feedback:

- `SSLSocket#syswrite_nonblock`:
  - Use `string` (= `String | _ToStr`) instead of `_ToS` for the
    argument: the C implementation uses `StringValue`, which converts
    via `to_str`, so `nil`, `Hash`, etc. are not accepted.
  - Remove `nil` from the `exception: false` return: unlike
    `sysread_nonblock`, write has no EOF-equivalent path that returns
    `nil`; it raises on real errors and only converts wait-state into
    `:wait_readable` / `:wait_writable`.
- `BasicSocket#recv_nonblock`, `BasicSocket#recvmsg_nonblock`,
  `Socket#recvfrom_nonblock`, `UDPSocket#recvfrom_nonblock`: include
  `nil` in both `exception: true` and `exception: false` overloads.
  The rdoc states that these methods return `nil` when the underlying
  `recvfrom(2)` returns 0 (closed connection or empty datagram),
  regardless of `exception:`.
@ksss ksss added this to the RBS 4.1 milestone May 28, 2026
# * Socket#recvfrom
#
def recvfrom_nonblock: (Integer maxlen, ?Integer flags, ?untyped outbuf, ?exception: boolish) -> ([ String, Addrinfo ] | :wait_readable)
def recvfrom_nonblock: (Integer maxlen, ?Integer flags, ?untyped outbuf, ?exception: true) -> [ String, Addrinfo ]?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code told me that nil won't return from this method. Is it correct?

#
def sysread_nonblock: (*untyped) -> untyped
def sysread_nonblock: (Integer length, ?String buffer, ?exception: true) -> String
| (Integer length, ?String buffer, exception: false) -> (String | :wait_readable | :wait_writable | nil)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| (Integer length, ?String buffer, exception: false) -> (String | :wait_readable | :wait_writable | nil)
| (Integer length, ?String buffer, exception: boolish) -> (String | :wait_readable | :wait_writable | nil)

exception: keyword parameter must be wider than false, because the precise type information may be lost: exception = rand() > 0.5 #: bool.

I found exception: accepts any object (..., exception: "true"), but it's really complicated to support these cases. So, the assumption, programmers will write the exception: true, would make sense.

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.

3 participants