fix: make bpn response failures diagnosable

This commit is contained in:
2026-09-19 11:51:15 +02:00
parent ec83ad598f
commit fbb3ebb8c1
5 changed files with 240 additions and 29 deletions
+73 -24
View File
@@ -602,10 +602,12 @@ class ApiClient
'port' => $this->selectedPort,
]);
$this->send($socket, $body);
$response = $this->receive($socket);
$this->disconnect($socket);
$responseXml = substr($response, 10);
try {
$responseXml = $this->receive($socket);
} finally {
$this->disconnect($socket);
}
if (true === $debug || true === $this->config['debug']) {
$this->dumpXmlToFile('response', $requestId, $responseXml);
@@ -714,18 +716,12 @@ class ApiClient
]);
$this->send($socket, $body);
$response = $this->receive($socket);
$this->disconnect($socket);
// 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');
try {
// the length header is consumed and verified by receive(), this is the payload
$xml = $this->receive($socket);
} finally {
$this->disconnect($socket);
}
if (true === $debug || true === $this->config['debug']) {
@@ -735,14 +731,19 @@ class ApiClient
try {
return $this->responseParser->parseXmlString($type, $xml, $additionalArgs);
} catch (ResponseParserException $e) {
// Keep the evidence even outside debug mode: without the raw response a parse
// failure is not diagnosable after the fact. app:cleanup:xml-dumps prunes it.
$this->dumpXmlToFile('response', $requestId, $xml);
$this->logger->error('Unable to parse response received from API', [
'request_id' => $requestId,
'type' => $type,
'response_length' => strlen($xml),
'error_message' => $e->getMessage(),
]);
throw new ApiClientException('Unable to parse response received from API: '.$e->getMessage(), 0, $e);
}
$this->logger->error('Unexpected response received from API', [
'request_id' => $requestId,
]);
throw new ApiClientException('Unexpected response received from API');
}
/**
@@ -773,10 +774,12 @@ class ApiClient
]);
$this->send($socket, $body);
$response = $this->receive($socket);
$this->disconnect($socket);
$xml = substr($response, 10);
try {
$xml = $this->receive($socket);
} finally {
$this->disconnect($socket);
}
if (true === $this->config['debug']) {
$this->dumpXmlToFile('response', $requestId, $xml);
@@ -898,8 +901,14 @@ class ApiClient
}
/**
* Reads a single response message and returns its payload. The protocol prepends the
* payload length as a 10 byte header, so the announced length is compared against what
* actually arrived: a peer closing mid-stream would otherwise yield a silently
* truncated body that only surfaces later as an unexplained parse failure.
*
* @param resource $socket
*
* @throws ApiClientException
* @throws TimeoutException
* @throws ImmediateConnectionCloseException
*/
@@ -963,7 +972,47 @@ class ApiClient
throw new ImmediateConnectionCloseException('Server closed connection without sending data');
}
return $response;
return $this->extractPayload($response);
}
/**
* @throws ApiClientException
*/
private function extractPayload(string $response): string
{
if (10 > strlen($response)) {
$this->logger->error('Incomplete response header received from API', [
'bytes_received' => strlen($response),
'port' => $this->selectedPort,
]);
throw new ApiClientException(sprintf('Incomplete response header, got %d of 10 bytes', strlen($response)));
}
$header = substr($response, 0, 10);
$announcedLength = (int) trim($header);
$payload = substr($response, 10);
if (1 > $announcedLength) {
$this->logger->error('Empty response body announced by API', [
'header' => trim($header),
'port' => $this->selectedPort,
]);
throw new ApiClientException(sprintf('Response announced an empty body (header "%s")', trim($header)));
}
if (strlen($payload) !== $announcedLength) {
$this->logger->error('Truncated response received from API', [
'bytes_received' => strlen($payload),
'announced_length' => $announcedLength,
'port' => $this->selectedPort,
]);
throw new ApiClientException(sprintf('Truncated response, got %d of %d announced bytes', strlen($payload), $announcedLength));
}
return $payload;
}
/** @param resource $socket */