@@ -447,6 +447,7 @@ actor Listener(cap: net.TCPListenCap, address: str, port: int, on_listen_error:
447447# TODO: default schema="https"
448448# TODO: default port=None
449449# TODO: default tls_verify=True
450+ # TODO: add arguments for configuring request buffering (size) and reconnection attempts and timeouts
450451actor Client(cap: net.TCPConnectCap, scheme: str, address: str, port: ?int, tls_verify: bool, on_connect: action(Client) -> None, on_error: action(Client, str) -> None, log_handler: ?logging.Handler):
451452 """HTTP(S) Client
452453
@@ -457,10 +458,11 @@ actor Client(cap: net.TCPConnectCap, scheme: str, address: str, port: ?int, tls_
457458 var _on_response: list[(bytes, action(Client, Response) -> None)] = []
458459 var version: ?bytes = None
459460 var buf = b""
460- var close_connection: bool = True
461461 var tcp_conn: ?net.TCPConnection = None
462462 var tls_conn: ?net.TLSConnection = None
463463
464+ var connecting: bool = True
465+
464466 def _connect():
465467 if scheme == "http":
466468 _log.verbose("Using http scheme and port 80", None)
@@ -475,9 +477,18 @@ actor Client(cap: net.TCPConnectCap, scheme: str, address: str, port: ?int, tls_
475477
476478 def _on_conn_connect():
477479 # If there are outstanding requests, it probably means we were
480+ # disconnected or have not connected yet
481+ # TODO: do not flush entire buffer if we know the server will close the
482+ # connection. If the latency is so big that we're able to send the
483+ # entire buffer before the server closes the connection we're just
484+ # wasting resources.
478485 for r in _on_response:
486+ _log.trace("Sending outstanding request", {"request": r.0})
479487 _conn_write(r.0)
480- await async on_connect(self)
488+ if connecting:
489+ # Dispatch the on_connect callback on first connect but not for reconnects
490+ await async on_connect(self)
491+ connecting = False
481492
482493 def _on_tcp_connect(conn: net.TCPConnection) -> None:
483494 _on_conn_connect()
@@ -500,10 +511,10 @@ actor Client(cap: net.TCPConnectCap, scheme: str, address: str, port: ?int, tls_
500511 r, buf = parse_response(buf, _log)
501512 if r is not None:
502513 if "connection" in r.headers and r.headers["connection"] == "close":
503- close_connection = True
504- _conn_close()
514+ # Is this really the right thing to do here? If the client
515+ # does not make a new request we just reconnected for nothing!
505516 _log.debug("Closing TCP connection due to header: Connection: close", None)
506- _connect ()
517+ _conn_reconnect ()
507518 if len(_on_response) == 0:
508519 _log.notice("Data received with no on_response callback set", None)
509520 break
@@ -524,22 +535,31 @@ actor Client(cap: net.TCPConnectCap, scheme: str, address: str, port: ?int, tls_
524535 def _on_con_error(error: str) -> None:
525536 on_error(self, error)
526537
527- def _conn_close () -> None:
538+ def _conn_reconnect () -> None:
528539 if tcp_conn is not None:
529- def _noop(c):
530- pass
531- tcp_conn.close(_noop)
540+ tcp_conn.reconnect()
532541 elif tls_conn is not None:
533- def _noop(c):
534- pass
535- tls_conn.close(_noop)
542+ tls_conn.reconnect()
536543
537544 def _conn_write(data: bytes) -> None:
538- _log.trace("Sending data", {"data": data})
539- if tcp_conn is not None:
540- tcp_conn.write(data)
541- elif tls_conn is not None:
542- tls_conn.write(data)
545+ try:
546+ _log.trace("Sending data", {"data": data})
547+ # We call the write method on the TCP or TLS connection actor
548+ # synchronously because we want to be able to catch exceptions
549+ # signaling the socket was closed. Note that this is *not* waiting
550+ # for network I/O, just waiting on system I/O for writing to the
551+ # local socket buffer.
552+ if tcp_conn is not None:
553+ await async tcp_conn.write(data)
554+ elif tls_conn is not None:
555+ await async tls_conn.write(data)
556+ except RuntimeError as exc:
557+ # HTTP/1.0 servers close the connection after each request by default
558+ if "bad file descriptor" in str(exc) or "bad stream" in str(exc):
559+ _log.debug("TCP connection closed, reconnecting", {"error": str(exc)})
560+ _conn_reconnect()
561+ else:
562+ _on_con_error(str(exc))
543563
544564 # HTTP methods
545565 def get(path: str, headers: dict[str, str], on_response: action(Client, Response) -> None):
0 commit comments