diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php index 9b5013d..4113b55 100644 --- a/src/BusProNet/ApiClient.php +++ b/src/BusProNet/ApiClient.php @@ -688,6 +688,14 @@ class ApiClient // message length (10 bytes) is prepended to actual message $xml = substr($response, 10); + if ('' === $xml) { + $this->logger->error('Empty response body received from API (header-only response)', [ + 'request_id' => $requestId, + 'raw_length' => strlen($response), + ]); + throw new ApiClientException('Empty response body received from API'); + } + if (true === $debug || true === $this->config['debug']) { $this->dumpXmlToFile('response', $requestId, $xml); } @@ -898,6 +906,15 @@ class ApiClient $response .= $chunk; } + if (0 === strlen($response)) { + $elapsedTime = microtime(true) - $this->operationStartTime; + $this->logger->warning('Server closed connection without sending data', [ + 'elapsed_time' => $elapsedTime, + 'read_attempts' => $readAttempts, + ]); + throw new ImmediateConnectionCloseException('Server closed connection without sending data'); + } + return $response; } diff --git a/tests/BusProNet/ApiClientReceiveTest.php b/tests/BusProNet/ApiClientReceiveTest.php new file mode 100644 index 0000000..f91730f --- /dev/null +++ b/tests/BusProNet/ApiClientReceiveTest.php @@ -0,0 +1,78 @@ +apiClient = new ApiClient( + $this->createMock(SerializerInterface::class), + $this->createMock(ApiResponseParser::class), + $this->createMock(FilesystemOperator::class), + $this->createMock(LoggerInterface::class), + $this->createMock(BookingDataProcessor::class), + new RequestStack(), + [ + 'bpn_username' => 'test', + 'bpn_password' => 'test', + 'bpn_api_ip' => '127.0.0.1', + 'bpn_api_ports' => [9000], + ], + ); + } + + public function testEmptyResponseThrowsImmediateConnectionCloseException(): void + { + $stream = fopen('php://memory', 'r+'); + // Write nothing — stream is immediately at EOF + rewind($stream); + + $this->setOperationStartTime(); + + $this->expectException(ImmediateConnectionCloseException::class); + $this->expectExceptionMessage('Server closed connection without sending data'); + + $this->invokeReceive($stream); + } + + public function testNonEmptyResponseReturnsData(): void + { + $stream = fopen('php://memory', 'r+'); + fwrite($stream, '0000000005Hello'); + rewind($stream); + + $this->setOperationStartTime(); + + $result = $this->invokeReceive($stream); + + self::assertSame('0000000005Hello', $result); + } + + private function invokeReceive($socket): string + { + $method = new \ReflectionMethod(ApiClient::class, 'receive'); + + return $method->invoke($this->apiClient, $socket); + } + + private function setOperationStartTime(): void + { + $property = new \ReflectionProperty(ApiClient::class, 'operationStartTime'); + $property->setValue($this->apiClient, microtime(true)); + } +}