fix: handle empty api responses more gracefully
This commit is contained in:
@@ -688,6 +688,14 @@ class ApiClient
|
|||||||
// message length (10 bytes) is prepended to actual message
|
// message length (10 bytes) is prepended to actual message
|
||||||
$xml = substr($response, 10);
|
$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']) {
|
if (true === $debug || true === $this->config['debug']) {
|
||||||
$this->dumpXmlToFile('response', $requestId, $xml);
|
$this->dumpXmlToFile('response', $requestId, $xml);
|
||||||
}
|
}
|
||||||
@@ -898,6 +906,15 @@ class ApiClient
|
|||||||
$response .= $chunk;
|
$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;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\BusProNet;
|
||||||
|
|
||||||
|
use App\BusProNet\ApiClient;
|
||||||
|
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||||
|
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||||
|
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||||
|
use League\Flysystem\FilesystemOperator;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
|
class ApiClientReceiveTest extends TestCase
|
||||||
|
{
|
||||||
|
private ApiClient $apiClient;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user