Skip to content

Commit a4a5a0b

Browse files
committed
Added more error handling around signal HTTP errors
1 parent 65b376d commit a4a5a0b

File tree

7 files changed

+198
-30
lines changed

7 files changed

+198
-30
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace OpenTok\Exception;
4+
5+
/**
6+
* Defines an exception thrown when a call to a signal method results in no
7+
* response from the server
8+
*/
9+
class SignalNetworkConnectionException extends \RuntimeException implements SignalException
10+
{
11+
12+
}

src/OpenTok/Util/Client.php

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use GuzzleHttp\Psr7\Request;
1010
use Psr\Http\Message\RequestInterface;
1111
use Firebase\JWT\JWT;
12+
use GuzzleHttp\Exception\RequestException;
1213
use OpenTok\Exception\Exception;
1314
use OpenTok\Exception\DomainException;
1415
use OpenTok\Exception\UnexpectedValueException;
@@ -27,6 +28,7 @@
2728
use OpenTok\Exception\ForceDisconnectConnectionException;
2829
use OpenTok\Exception\ForceDisconnectUnexpectedValueException;
2930
use OpenTok\Exception\ForceDisconnectAuthenticationException;
31+
use OpenTok\Exception\SignalNetworkConnectionException;
3032
use OpenTok\MediaMode;
3133

3234
use function GuzzleHttp\default_user_agent;
@@ -45,38 +47,47 @@ class Client
4547
protected $apiKey;
4648
protected $apiSecret;
4749
protected $configured = false;
50+
51+
/**
52+
* @var \GuzzleHttp\Client
53+
*/
4854
protected $client;
4955

5056
public function configure($apiKey, $apiSecret, $apiUrl, $options = array())
5157
{
5258
$this->apiKey = $apiKey;
5359
$this->apiSecret = $apiSecret;
5460

55-
$clientOptions = [
56-
'base_uri' => $apiUrl,
57-
'headers' => [
58-
'User-Agent' => OPENTOK_SDK_USER_AGENT . ' ' . default_user_agent(),
59-
],
60-
];
61+
if (isset($options['client'])) {
62+
$this->client = $options['client'];
63+
} else {
64+
$clientOptions = [
65+
'base_uri' => $apiUrl,
66+
'headers' => [
67+
'User-Agent' => OPENTOK_SDK_USER_AGENT . ' ' . default_user_agent(),
68+
],
69+
];
70+
71+
if (!empty($options['timeout'])) {
72+
$clientOptions['timeout'] = $options['timeout'];
73+
}
6174

62-
if (!empty($options['timeout'])) {
63-
$clientOptions['timeout'] = $options['timeout'];
64-
}
75+
if (empty($options['handler'])) {
76+
$handlerStack = HandlerStack::create();
77+
} else {
78+
$handlerStack = $options['handler'];
79+
}
80+
$clientOptions['handler'] = $handlerStack;
6581

66-
if (empty($options['handler'])) {
67-
$handlerStack = HandlerStack::create();
68-
} else {
69-
$handlerStack = $options['handler'];
70-
}
71-
$clientOptions['handler'] = $handlerStack;
82+
$handler = Middleware::mapRequest(function (RequestInterface $request) {
83+
$authHeader = $this->createAuthHeader();
84+
return $request->withHeader('X-OPENTOK-AUTH', $authHeader);
85+
});
86+
$handlerStack->push($handler);
7287

73-
$handler = Middleware::mapRequest(function (RequestInterface $request) {
74-
$authHeader = $this->createAuthHeader();
75-
return $request->withHeader('X-OPENTOK-AUTH', $authHeader);
76-
});
77-
$handlerStack->push($handler);
88+
$this->client = new \GuzzleHttp\Client($clientOptions);
89+
}
7890

79-
$this->client = new \GuzzleHttp\Client($clientOptions);
8091
$this->configured = true;
8192
}
8293

@@ -504,27 +515,42 @@ public function dial($sessionId, $token, $sipUri, $options)
504515
return $sipJson;
505516
}
506517

507-
public function signal($sessionId, $options = array(), $connectionId = null)
518+
/**
519+
* Signal either an entire session or a specific connection in a session
520+
*
521+
* @param string $sessionId ID of the session to send the signal to
522+
* @param array{type: string, data: mixed} $payload Signal payload to send
523+
* @param string $connectionId ID of the connection to send the signal to
524+
*
525+
* @todo Mark $payload as required, as you cannot send an empty signal request body
526+
*
527+
* @throws SignalNetworkConnectionException
528+
* @throws \Exception
529+
*/
530+
public function signal($sessionId, $payload = [], $connectionId = null)
508531
{
509532
// set up the request
510-
511-
533+
$requestRoot = '/v2/project/' . $this->apiKey . '/session/' . $sessionId;
512534
$request = is_null($connectionId) || empty($connectionId) ?
513-
new Request('POST', '/v2/project/' . $this->apiKey . '/session/' . $sessionId . '/signal')
514-
: new Request('POST', '/v2/project/' . $this->apiKey . '/session/' . $sessionId . '/connection/' . $connectionId . '/signal');
535+
new Request('POST', $requestRoot . '/signal')
536+
: new Request('POST', $requestRoot . '/connection/' . $connectionId . '/signal');
515537

516538
try {
517539
$response = $this->client->send($request, [
518540
'debug' => $this->isDebug(),
519541
'json' => array_merge(
520-
$options
542+
$payload
521543
)
522544
]);
523545
if ($response->getStatusCode() != 204) {
524546
json_decode($response->getBody(), true);
525547
}
526-
} catch (\Exception $e) {
548+
} catch (ClientException $e) {
527549
$this->handleSignalingException($e);
550+
} catch (RequestException $e) {
551+
throw new SignalNetworkConnectionException('Unable to communicate with host', -1, $e);
552+
} catch (\Exception $e) {
553+
throw $e;
528554
}
529555
}
530556

@@ -611,7 +637,7 @@ private function handleBroadcastException($e)
611637
}
612638
}
613639

614-
private function handleSignalingException($e)
640+
private function handleSignalingException(ClientException $e)
615641
{
616642
$responseCode = $e->getResponse()->getStatusCode();
617643
switch ($responseCode) {
@@ -624,7 +650,8 @@ private function handleSignalingException($e)
624650
$message = 'The client specified by the connectionId property is not connected to the session.';
625651
throw new SignalConnectionException($message, $responseCode);
626652
case 413:
627-
$message = 'The type string exceeds the maximum length (128 bytes), or the data string exceeds the maximum size (8 kB).';
653+
$message = 'The type string exceeds the maximum length (128 bytes),'
654+
. ' or the data string exceeds the maximum size (8 kB).';
628655
throw new SignalUnexpectedValueException($message, $responseCode);
629656
default:
630657
break;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace OpenTokTest\Util;
4+
5+
use GuzzleHttp\Client as GuzzleHttpClient;
6+
use GuzzleHttp\Exception\RequestException;
7+
use GuzzleHttp\Handler\MockHandler;
8+
use GuzzleHttp\HandlerStack;
9+
use GuzzleHttp\Psr7\Request;
10+
use GuzzleHttp\Psr7\Response;
11+
use OpenTok\Exception\SignalAuthenticationException;
12+
use OpenTok\Exception\SignalConnectionException;
13+
use OpenTok\Exception\SignalNetworkConnectionException;
14+
use OpenTok\Exception\SignalUnexpectedValueException;
15+
use OpenTok\Util\Client;
16+
use PHPUnit\Framework\TestCase;
17+
use Prophecy\Argument;
18+
use Psr\Http\Message\RequestInterface;
19+
20+
class ClientTest extends TestCase
21+
{
22+
public function testHandlesSignalErrorHandlesNoResponse()
23+
{
24+
$this->expectException(SignalNetworkConnectionException::class);
25+
$this->expectExceptionMessage('Unable to communicate with host');
26+
27+
$mock = new MockHandler([
28+
new RequestException('Unable to communicate with host', new Request('GET', 'signals')),
29+
]);
30+
$handlerStack = HandlerStack::create($mock);
31+
$guzzle = new GuzzleHttpClient(['handler' => $handlerStack]);
32+
33+
$client = new Client();
34+
$client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]);
35+
$client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234');
36+
}
37+
38+
public function testHandlesSignalErrorHandles400Response()
39+
{
40+
$this->expectException(SignalUnexpectedValueException::class);
41+
42+
$mock = new MockHandler([
43+
$this->getResponse('signal-failure-payload', 400)
44+
]);
45+
$handlerStack = HandlerStack::create($mock);
46+
$guzzle = new GuzzleHttpClient(['handler' => $handlerStack]);
47+
48+
$client = new Client();
49+
$client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]);
50+
$client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234');
51+
}
52+
53+
public function testHandlesSignalErrorHandles403Response()
54+
{
55+
$this->expectException(SignalAuthenticationException::class);
56+
57+
$mock = new MockHandler([
58+
$this->getResponse('signal-failure-invalid-token', 403)
59+
]);
60+
$handlerStack = HandlerStack::create($mock);
61+
$guzzle = new GuzzleHttpClient(['handler' => $handlerStack]);
62+
63+
$client = new Client();
64+
$client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]);
65+
$client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234');
66+
}
67+
68+
public function testHandlesSignalErrorHandles404Response()
69+
{
70+
$this->expectException(SignalConnectionException::class);
71+
$this->expectExceptionMessage('The client specified by the connectionId property is not connected to the session.');
72+
73+
$mock = new MockHandler([
74+
$this->getResponse('signal-failure-no-clients', 404)
75+
]);
76+
$handlerStack = HandlerStack::create($mock);
77+
$guzzle = new GuzzleHttpClient(['handler' => $handlerStack]);
78+
79+
$client = new Client();
80+
$client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]);
81+
$client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234');
82+
}
83+
84+
public function testHandlesSignalErrorHandles413Response()
85+
{
86+
$this->expectException(SignalUnexpectedValueException::class);
87+
$this->expectExceptionMessage('The type string exceeds the maximum length (128 bytes), or the data string exceeds the maximum size (8 kB).');
88+
89+
$mock = new MockHandler([
90+
$this->getResponse('signal-failure', 413)
91+
]);
92+
$handlerStack = HandlerStack::create($mock);
93+
$guzzle = new GuzzleHttpClient(['handler' => $handlerStack]);
94+
95+
$client = new Client();
96+
$client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]);
97+
$client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234');
98+
}
99+
100+
/**
101+
* Get the API response we'd expect for a call to the API.
102+
*/
103+
protected function getResponse(string $type = 'success', int $status = 200): Response
104+
{
105+
return new Response(
106+
$status,
107+
['Content-Type' => 'application/json'],
108+
fopen(__DIR__ . '/responses/' . $type . '.json', 'rb')
109+
);
110+
}
111+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"code": -1,
3+
"message": "Id in the token does not match the one in the url",
4+
"description": "Id in the token does not match the one in the url"
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"message": "Not found. No clients are actively connected to the OpenTok session."
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"code": 15202,
3+
"message": "Signal payload 'data' must be set",
4+
"description": "Signal payload 'data' must be set"
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"code": -99,
3+
"message": "Unknown error",
4+
"description": "An unknown error occurred"
5+
}

0 commit comments

Comments
 (0)