···286286 return self.stream.writeTimeout(ms);
287287 }
288288289289- pub fn readTimeout(self: *const Client, ms: u32) !void {
289289+ pub fn readTimeout(self: *Client, ms: u32) !void {
290290 return self.stream.readTimeout(ms);
291291 }
292292···400400 io: Io,
401401 stream: Io.net.Stream,
402402 tls_client: ?*TLSClient = null,
403403+ read_timeout_ms: u32 = 0,
403404404405 pub fn init(io: Io, stream: Io.net.Stream, tls_client: ?*TLSClient) Stream {
405406 return .{
···441442 }
442443443444 pub fn read(self: *Stream, buf: []u8) !usize {
445445+ // A read timeout is implemented by polling the socket for readiness
446446+ // rather than SO_RCVTIMEO. On the TLS path the underlying read goes
447447+ // through std.crypto.tls, whose reader treats a socket EAGAIN (what
448448+ // SO_RCVTIMEO produces on timeout) as a programmer bug and panics in
449449+ // debug builds. Polling lets a timeout surface as error.WouldBlock (which
450450+ // read() turns into "no message") without ever issuing a read that could
451451+ // EAGAIN.
452452+ //
453453+ // Skip the poll when the TLS client already has decrypted plaintext
454454+ // buffered: a previous read can decrypt more than the caller consumed, so
455455+ // the socket can be empty (poll would time out) even though data is
456456+ // available in-process, which would starve it.
457457+ const tls_buffered = if (self.tls_client) |tls_client| tls_client.client.reader.bufferedLen() else 0;
458458+ if (self.read_timeout_ms > 0 and tls_buffered == 0) {
459459+ var pfd = [_]std.posix.pollfd{.{
460460+ .fd = self.stream.socket.handle,
461461+ .events = std.posix.POLL.IN,
462462+ .revents = 0,
463463+ }};
464464+ // A poll failure is a real read failure, not "no data": surface it
465465+ // (mapped into this read path's error set) rather than swallowing it.
466466+ const ready = std.posix.poll(&pfd, @intCast(self.read_timeout_ms)) catch return error.ReadFailed;
467467+ if (ready == 0) return error.WouldBlock;
468468+ }
444469 if (self.tls_client) |tls_client| {
445470 var w: std.Io.Writer = .fixed(buf);
446471 while (true) {
···473498 return self.setTimeout(posix.SO.SNDTIMEO, ms);
474499 }
475500476476- pub fn readTimeout(self: *const Stream, ms: u32) !void {
477477- return self.setTimeout(posix.SO.RCVTIMEO, ms);
501501+ pub fn readTimeout(self: *Stream, ms: u32) !void {
502502+ // Stored and applied via poll() in read(); see the note there for why this
503503+ // does not use SO_RCVTIMEO.
504504+ self.read_timeout_ms = ms;
478505 }
479506480507 fn setTimeout(self: *const Stream, opt_name: u32, ms: u32) !void {