[Client] Throw TimeoutException on request timeout and cancel the req… · modelcontextprotocol/php-sdk@91f7089 · GitHub
Skip to content

Commit 91f7089

Browse files
authored
[Client] Throw TimeoutException on request timeout and cancel the request (#567)
The default request timeout now runs as a sub-second deadline like the per-call one.
1 parent 10938db commit 91f7089

10 files changed

Lines changed: 54 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions

‎docs/client/errors.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ try {
4040
}
4141
```
4242

43+
## TimeoutException
44+
45+
Thrown when the server does not answer within the request timeout. It extends
46+
`RequestException`, so catch it first to tell a timeout from an error response:
47+
48+
```php
49+
use Mcp\Exception\RequestException;
50+
use Mcp\Exception\TimeoutException;
51+
52+
try {
53+
$result = $client->callTool('slow_tool', []);
54+
} catch (TimeoutException $e) {
55+
echo "The server did not answer in time\n";
56+
} catch (RequestException $e) {
57+
echo "Request failed: {$e->getMessage()}\n";
58+
}
59+
```
60+
4361
## Complete Example
4462

4563
Here's a comprehensive example demonstrating client usage:

‎docs/client/transports.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ $transport = new HttpTransport('http://localhost:8000', listen: true);
126126
`timeoutSeconds: ?float` arguments. The token's `isCancellationRequested()`
127127
method must return without blocking. The timeout must be finite and positive
128128
and replaces the default request timeout for this call.
129-
An observed cancellation throws `RequestCancelledException`. An observed
130-
per-call deadline expiry throws `TimeoutException`.
129+
An observed cancellation throws `RequestCancelledException`. When the per-call
130+
or default request timeout runs out, the client throws `TimeoutException`, a
131+
`RequestException`, and sends `notifications/cancelled` for the request.
131132

132133
Interruption is checked before a request goes out and again once the send
133134
returns. The second check is what covers a synchronous `application/json`

‎src/Client.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ public function listTools(?string $cursor = null): ListToolsResult
226226
* @param float|null $timeoutSeconds Finite positive timeout replacing the default for this call
227227
*
228228
* @throws RequestCancelledException When cancellation is observed
229-
* @throws Exception\TimeoutException When the per-call deadline is observed to have expired
229+
* @throws Exception\TimeoutException When the request timeout is observed to have expired
230230
* @throws RequestException|ConnectionException|InvalidArgumentException|RuntimeException
231231
*/
232232
public function callTool(string $name, array $arguments = [], ?callable $onProgress = null, ?CancellationTokenInterface $cancellation = null, ?float $timeoutSeconds = null): CallToolResult
@@ -407,7 +407,7 @@ private function sendRequest(Request $request, ?callable $onProgress = null, ?Ca
407407
}
408408

409409
$withProgress = null !== $onProgress;
410-
$fiber = new \Fiber(fn () => $this->protocol->request($request, $this->config->requestTimeout, $withProgress, $cancellation, $timeoutSeconds));
410+
$fiber = new \Fiber(fn () => $this->protocol->request($request, $this->config->requestTimeout, $withProgress, $cancellation, $timeoutSeconds ?? $this->config->requestTimeout));
411411
$this->protocol->setProgressCallback($onProgress);
412412

413413
try {

‎src/Client/State/ClientState.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ClientState implements ClientStateInterface
3535
private ?string $instructions = null;
3636
private ?ServerCapabilities $serverCapabilities = null;
3737

38-
/** @var array<int|string, array{request_id: int|string, timestamp: int, timeout: int}> */
38+
/** @var array<int|string, array{request_id: int|string, timestamp: float, timeout: int}> */
3939
private array $pendingRequests = [];
4040

4141
/** @var array<int|string, array<string, mixed>> */
@@ -53,7 +53,7 @@ public function addPendingRequest(int|string $requestId, int $timeout): void
5353
{
5454
$this->pendingRequests[$requestId] = [
5555
'request_id' => $requestId,
56-
'timestamp' => time(),
56+
'timestamp' => microtime(true),
5757
'timeout' => $timeout,
5858
];
5959
}

‎src/Client/State/ClientStateInterface.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function removePendingRequest(int|string $requestId): void;
4949
/**
5050
* Get all pending requests.
5151
*
52-
* @return array<int|string, array{request_id: int|string, timestamp: int, timeout: int}>
52+
* @return array<int|string, array{request_id: int|string, timestamp: float, timeout: int}>
5353
*/
5454
public function getPendingRequests(): array;
5555

‎src/Client/Transport/HttpTransport.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ private function processFiber(): void
985985
}
986986

987987
// The explicit per-call deadline replaces the default request timeout.
988-
if (null === ($this->activeSuspend['deadline'] ?? null) && time() - $timestamp >= $timeout) {
988+
if (null === ($this->activeSuspend['deadline'] ?? null) && microtime(true) - $timestamp >= $timeout) {
989989
$this->logger->warning('Request timed out', ['request_id' => $requestId]);
990990
$error = Error::forInternalError('Request timed out', $requestId);
991991
$this->activeSuspend = $this->activeFiber->resume($error);

‎src/Client/Transport/StdioTransport.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ private function processFiber(): void
363363
}
364364

365365
// Check timeout
366-
if (time() - $timestamp >= $timeout) {
366+
if (microtime(true) - $timestamp >= $timeout) {
367367
$this->logger->warning('Request timed out', ['request_id' => $requestId]);
368368
$error = Error::forInternalError('Request timed out', $requestId);
369369
$this->activeSuspend = $this->activeFiber->resume($error);

‎src/Exception/TimeoutException.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
*
1717
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
1818
*/
19-
class TimeoutException extends Exception
19+
class TimeoutException extends RequestException
2020
{
2121
}

‎tests/Integration/CancellationTest.php‎

Lines changed: 23 additions & 0 deletions

0 commit comments

Comments
 (0)