3030use Laudis \Neo4j \Contracts \ConnectionInterface ;
3131use Laudis \Neo4j \Databags \BookmarkHolder ;
3232use Laudis \Neo4j \Databags \DatabaseInfo ;
33+ use Laudis \Neo4j \Databags \DriverConfiguration ;
3334use Laudis \Neo4j \Databags \Neo4jError ;
3435use Laudis \Neo4j \Enum \AccessMode ;
3536use Laudis \Neo4j \Enum \ConnectionProtocol ;
@@ -66,6 +67,10 @@ class BoltConnection implements ConnectionInterface
6667 */
6768 private array $ subscribedResults = [];
6869
70+ private ?float $ recvTimeoutHint = null ;
71+
72+ private ?float $ originalTimeout = null ;
73+
6974 /**
7075 * @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection}
7176 */
@@ -85,6 +90,7 @@ public function __construct(
8590 /** @psalm-readonly */
8691 private readonly ConnectionConfiguration $ config ,
8792 private readonly ?Neo4jLogger $ logger ,
93+ private readonly float $ defaultRecvTimeout = DriverConfiguration::DEFAULT_SOCKET_TIMEOUT ,
8894 ) {
8995 $ this ->messageFactory = new BoltMessageFactory ($ this , $ this ->logger );
9096 }
@@ -171,7 +177,17 @@ public function isStreaming(): bool
171177
172178 public function setTimeout (float $ timeout ): void
173179 {
174- $ this ->connection ->setTimeout ($ timeout );
180+ // Only set timeout if connection is still open
181+ // This prevents errors when trying to set timeout on a closed socket
182+ // Connection::setTimeout swallows errors on closed connections (cleanup scenario)
183+ if ($ this ->isOpen ()) {
184+ $ this ->connection ->setTimeout ($ timeout );
185+ }
186+ }
187+
188+ public function getTimeout (): float
189+ {
190+ return $ this ->connection ->getTimeout ();
175191 }
176192
177193 public function consumeResults (): void
@@ -300,13 +316,36 @@ public function pull(?int $qid, ?int $fetchSize): array
300316 $ tbr = [];
301317 $ message = $ this ->messageFactory ->createPullMessage ($ extra );
302318
303- foreach ($ message ->send ()->getResponses () as $ response ) {
304- $ this ->assertNoFailure ($ response );
305- $ tbr [] = $ response ->content ;
306- }
319+ try {
320+ // Apply timeout before iterating to ensure disconnects are detected
321+ $ this ->applyRecvTimeoutTemporarily ();
322+
323+ // If no timeout hint is set, apply a default timeout to prevent hanging on disconnect.
324+ if ($ this ->originalTimeout === null && $ this ->recvTimeoutHint === null ) {
325+ $ this ->originalTimeout = $ this ->connection ->getTimeout ();
326+ $ this ->connection ->setTimeout ($ this ->defaultRecvTimeout );
327+ }
307328
308- /** @var non-empty-list<list> */
309- return $ tbr ;
329+ foreach ($ message ->send ()->getResponses () as $ response ) {
330+ $ this ->assertNoFailure ($ response );
331+ $ tbr [] = $ response ->content ;
332+ }
333+
334+ $ this ->restoreOriginalTimeout ();
335+
336+ /** @var non-empty-list<list> */
337+ return $ tbr ;
338+ } catch (Throwable $ e ) {
339+ $ this ->restoreOriginalTimeout ();
340+ // If we've received some records before the disconnect, return them so first next() succeeds and second next() fails.
341+ if (!empty ($ tbr )) {
342+ $ tbr [] = [];
343+
344+ /** @var non-empty-list<list> */
345+ return $ tbr ;
346+ }
347+ throw $ e ;
348+ }
310349 }
311350
312351 public function __destruct ()
@@ -466,4 +505,45 @@ public function discardUnconsumedResults(): void
466505 $ this ->subscribedResults = [];
467506 }
468507 }
508+
509+ public function setRecvTimeoutHint (?float $ timeout ): void
510+ {
511+ $ this ->recvTimeoutHint = $ timeout ;
512+ }
513+
514+ public function getRecvTimeoutHint (): ?float
515+ {
516+ return $ this ->recvTimeoutHint ;
517+ }
518+
519+ public function applyRecvTimeoutTemporarily (): void
520+ {
521+ if ($ this ->recvTimeoutHint !== null && $ this ->originalTimeout === null ) {
522+ $ this ->originalTimeout = $ this ->connection ->getTimeout ();
523+ $ this ->connection ->setTimeout ($ this ->recvTimeoutHint );
524+ }
525+ }
526+
527+ public function restoreOriginalTimeout (): void
528+ {
529+ if ($ this ->originalTimeout !== null ) {
530+ $ this ->connection ->setTimeout ($ this ->originalTimeout );
531+ $ this ->originalTimeout = null ;
532+ }
533+ }
534+
535+ public function getOriginalTimeout (): ?float
536+ {
537+ return $ this ->originalTimeout ;
538+ }
539+
540+ public function getDefaultRecvTimeout (): float
541+ {
542+ return $ this ->defaultRecvTimeout ;
543+ }
544+
545+ public function setOriginalTimeout (?float $ timeout ): void
546+ {
547+ $ this ->originalTimeout = $ timeout ;
548+ }
469549}
0 commit comments