Skip to content

Commit 705737b

Browse files
committed
Use socket error codes (errnos) for connection rejections
1 parent a729faa commit 705737b

File tree

6 files changed

+246
-53
lines changed

6 files changed

+246
-53
lines changed

src/Factory.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ public function createConnection($uri)
165165
$parts = parse_url($uri);
166166
$uri = preg_replace('#:[^:/]*@#', ':***@', $uri);
167167
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'mysql') {
168-
return \React\Promise\reject(new \InvalidArgumentException('Invalid MySQL URI given'));
168+
return \React\Promise\reject(new \InvalidArgumentException(
169+
'Invalid MySQL URI given (EINVAL)',
170+
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22
171+
));
169172
}
170173

171174
$args = [];
@@ -191,7 +194,8 @@ public function createConnection($uri)
191194
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
192195
// connection cancelled, start with rejecting attempt, then clean up
193196
$reject(new \RuntimeException(
194-
'Connection to ' . $uri . ' cancelled'
197+
'Connection to ' . $uri . ' cancelled (ECONNABORTED)',
198+
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
195199
));
196200

197201
// either close successful connection or cancel pending connection attempt
@@ -213,9 +217,16 @@ public function createConnection($uri)
213217
$deferred->resolve($connection);
214218
});
215219
$command->on('error', function (\Exception $error) use ($deferred, $stream, $uri) {
220+
$const = '';
221+
$errno = $error->getCode();
222+
if ($error instanceof Exception) {
223+
$const = ' (EACCES)';
224+
$errno = \defined('SOCKET_EACCES') ? \SOCKET_EACCES : 13;
225+
}
226+
216227
$deferred->reject(new \RuntimeException(
217-
'Connection to ' . $uri . ' failed during authentication: ' . $error->getMessage(),
218-
$error->getCode(),
228+
'Connection to ' . $uri . ' failed during authentication: ' . $error->getMessage() . $const,
229+
$errno,
219230
$error
220231
));
221232
$stream->close();
@@ -237,7 +248,8 @@ public function createConnection($uri)
237248
return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) {
238249
if ($e instanceof TimeoutException) {
239250
throw new \RuntimeException(
240-
'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds'
251+
'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)',
252+
\defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110
241253
);
242254
}
243255
throw $e;

src/Io/Connection.php

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,25 @@ public function close()
157157
}
158158

159159
$this->state = self::STATE_CLOSED;
160+
$remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false;
160161
$this->stream->close();
161162

162163
// reject all pending commands if connection is closed
163164
while (!$this->executor->isIdle()) {
164165
$command = $this->executor->dequeue();
165-
$command->emit('error', [
166-
new \RuntimeException('Connection lost')
167-
]);
166+
assert($command instanceof CommandInterface);
167+
168+
if ($remoteClosed) {
169+
$command->emit('error', [new \RuntimeException(
170+
'Connection closed by peer (ECONNRESET)',
171+
\defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104
172+
)]);
173+
} else {
174+
$command->emit('error', [new \RuntimeException(
175+
'Connection closing (ECONNABORTED)',
176+
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
177+
)]);
178+
}
168179
}
169180

170181
$this->emit('close');
@@ -189,7 +200,10 @@ public function handleConnectionError($err)
189200
public function handleConnectionClosed()
190201
{
191202
if ($this->state < self::STATE_CLOSEING) {
192-
$this->emit('error', [new \RuntimeException('mysql server has gone away'), $this]);
203+
$this->emit('error', [new \RuntimeException(
204+
'Connection closed by peer (ECONNRESET)',
205+
\defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104
206+
)]);
193207
}
194208

195209
$this->close();
@@ -202,10 +216,13 @@ public function handleConnectionClosed()
202216
*/
203217
protected function _doCommand(CommandInterface $command)
204218
{
205-
if ($this->state === self::STATE_AUTHENTICATED) {
206-
return $this->executor->enqueue($command);
207-
} else {
208-
throw new Exception("Can't send command");
219+
if ($this->state !== self::STATE_AUTHENTICATED) {
220+
throw new \RuntimeException(
221+
'Connection ' . ($this->state === self::STATE_CLOSED ? 'closed' : 'closing'). ' (ENOTCONN)',
222+
\defined('SOCKET_ENOTCONN') ? \SOCKET_ENOTCONN : 107
223+
);
209224
}
225+
226+
return $this->executor->enqueue($command);
210227
}
211228
}

src/Io/Parser.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ protected function onSuccess()
307307
if ($command instanceof QueryCommand) {
308308
$command->affectedRows = $this->affectedRows;
309309
$command->insertId = $this->insertId;
310-
$command->warningCount = $this->warningCount;
310+
$command->warningCount = $this->warningCount;
311311
$command->message = $this->message;
312312
}
313313
$command->emit('success');
@@ -322,9 +322,10 @@ public function onClose()
322322
if ($command instanceof QuitCommand) {
323323
$command->emit('success');
324324
} else {
325-
$command->emit('error', [
326-
new \RuntimeException('Connection lost')
327-
]);
325+
$command->emit('error', [new \RuntimeException(
326+
'Connection closing (ECONNABORTED)',
327+
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
328+
)]);
328329
}
329330
}
330331
}

tests/FactoryTest.php

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,17 @@ public function testConnectWillRejectWhenGivenInvalidScheme()
5252

5353
$promise = $factory->createConnection('foo://127.0.0.1');
5454

55-
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException')));
55+
$promise->then(null, $this->expectCallableOnceWith(
56+
$this->logicalAnd(
57+
$this->isInstanceOf('InvalidArgumentException'),
58+
$this->callback(function (\InvalidArgumentException $e) {
59+
return $e->getMessage() === 'Invalid MySQL URI given (EINVAL)';
60+
}),
61+
$this->callback(function (\InvalidArgumentException $e) {
62+
return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22);
63+
})
64+
)
65+
));
5666
}
5767

5868
public function testConnectWillUseGivenHostAndGivenPort()
@@ -113,7 +123,10 @@ public function testConnectWithInvalidUriWillRejectWithoutConnecting()
113123
$this->logicalAnd(
114124
$this->isInstanceOf('InvalidArgumentException'),
115125
$this->callback(function (\InvalidArgumentException $e) {
116-
return $e->getMessage() === 'Invalid MySQL URI given';
126+
return $e->getMessage() === 'Invalid MySQL URI given (EINVAL)';
127+
}),
128+
$this->callback(function (\InvalidArgumentException $e) {
129+
return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22);
117130
})
118131
)
119132
));
@@ -153,9 +166,15 @@ public function testConnectWithInvalidPassRejectsWithAuthenticationError()
153166

154167
$promise->then(null, $this->expectCallableOnceWith(
155168
$this->logicalAnd(
156-
$this->isInstanceOf('Exception'),
157-
$this->callback(function (\Exception $e) {
158-
return !!preg_match("/^Connection to mysql:\/\/[^ ]* failed during authentication: Access denied for user '.*?'@'.*?' \(using password: YES\)$/", $e->getMessage());
169+
$this->isInstanceOf('RuntimeException'),
170+
$this->callback(function (\RuntimeException $e) {
171+
return !!preg_match("/^Connection to mysql:\/\/[^ ]* failed during authentication: Access denied for user '.*?'@'.*?' \(using password: YES\) \(EACCES\)$/", $e->getMessage());
172+
}),
173+
$this->callback(function (\RuntimeException $e) {
174+
return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
175+
}),
176+
$this->callback(function (\RuntimeException $e) {
177+
return !!preg_match("/^Access denied for user '.*?'@'.*?' \(using password: YES\)$/", $e->getPrevious()->getMessage());
159178
})
160179
)
161180
));
@@ -177,7 +196,20 @@ public function testConnectWillRejectWhenServerClosesConnection()
177196
$uri = $this->getConnectionString(['host' => $parts['host'], 'port' => $parts['port']]);
178197

179198
$promise = $factory->createConnection($uri);
180-
$promise->then(null, $this->expectCallableOnce());
199+
200+
$uri = preg_replace('/:[^:]*@/', ':***@', $uri);
201+
202+
$promise->then(null, $this->expectCallableOnceWith(
203+
$this->logicalAnd(
204+
$this->isInstanceOf('RuntimeException'),
205+
$this->callback(function (\RuntimeException $e) use ($uri) {
206+
return $e->getMessage() === 'Connection to mysql://' . $uri . ' failed during authentication: Connection closed by peer (ECONNRESET)';
207+
}),
208+
$this->callback(function (\RuntimeException $e) {
209+
return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104);
210+
})
211+
)
212+
));
181213

182214
Loop::run();
183215
}
@@ -194,9 +226,12 @@ public function testConnectWillRejectOnExplicitTimeoutDespiteValidAuth()
194226

195227
$promise->then(null, $this->expectCallableOnceWith(
196228
$this->logicalAnd(
197-
$this->isInstanceOf('Exception'),
198-
$this->callback(function (\Exception $e) use ($uri) {
199-
return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds';
229+
$this->isInstanceOf('RuntimeException'),
230+
$this->callback(function (\RuntimeException $e) use ($uri) {
231+
return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds (ETIMEDOUT)';
232+
}),
233+
$this->callback(function (\RuntimeException $e) {
234+
return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
200235
})
201236
)
202237
));
@@ -219,9 +254,12 @@ public function testConnectWillRejectOnDefaultTimeoutFromIniDespiteValidAuth()
219254

220255
$promise->then(null, $this->expectCallableOnceWith(
221256
$this->logicalAnd(
222-
$this->isInstanceOf('Exception'),
223-
$this->callback(function (\Exception $e) use ($uri) {
224-
return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds';
257+
$this->isInstanceOf('RuntimeException'),
258+
$this->callback(function (\RuntimeException $e) use ($uri) {
259+
return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds (ETIMEDOUT)';
260+
}),
261+
$this->callback(function (\RuntimeException $e) {
262+
return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
225263
})
226264
)
227265
));
@@ -366,7 +404,7 @@ public function testConnectWithValidAuthCanCloseOnlyOnce()
366404

367405
public function testConnectWithValidAuthCanCloseAndAbortPing()
368406
{
369-
$this->expectOutputString('connected.aborted pending (Connection lost).aborted queued (Connection lost).closed.');
407+
$this->expectOutputString('connected.aborted pending (Connection closing (ECONNABORTED)).aborted queued (Connection closing (ECONNABORTED)).closed.');
370408

371409
$factory = new Factory();
372410

@@ -401,13 +439,17 @@ public function testlConnectWillRejectWhenUnderlyingConnectorRejects()
401439
$factory = new Factory($loop, $connector);
402440
$promise = $factory->createConnection('user:[email protected]');
403441

404-
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
405-
$promise->then(null, $this->expectCallableOnceWith($this->callback(function ($e) {
406-
return ($e->getMessage() === 'Connection to mysql://user:***@127.0.0.1 failed: Failed');
407-
})));
408-
$promise->then(null, $this->expectCallableOnceWith($this->callback(function ($e) {
409-
return ($e->getCode() === 123);
410-
})));
442+
$promise->then(null, $this->expectCallableOnceWith(
443+
$this->logicalAnd(
444+
$this->isInstanceOf('RuntimeException'),
445+
$this->callback(function (\RuntimeException $e) {
446+
return $e->getMessage() === 'Connection to mysql://user:***@127.0.0.1 failed: Failed';
447+
}),
448+
$this->callback(function (\RuntimeException $e) {
449+
return $e->getCode() === 123;
450+
})
451+
)
452+
));
411453
}
412454

413455
public function provideUris()
@@ -457,7 +499,10 @@ public function testCancelConnectWillCancelPendingConnection($uri, $safe)
457499
$this->logicalAnd(
458500
$this->isInstanceOf('RuntimeException'),
459501
$this->callback(function (\RuntimeException $e) use ($safe) {
460-
return $e->getMessage() === 'Connection to ' . $safe . ' cancelled';
502+
return $e->getMessage() === 'Connection to ' . $safe . ' cancelled (ECONNABORTED)';
503+
}),
504+
$this->callback(function (\RuntimeException $e) {
505+
return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103);
461506
})
462507
)
463508
));
@@ -477,10 +522,17 @@ public function testCancelConnectWillCancelPendingConnectionWithRuntimeException
477522

478523
$promise->cancel();
479524

480-
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
481-
$promise->then(null, $this->expectCallableOnceWith($this->callback(function ($e) {
482-
return ($e->getMessage() === 'Connection to mysql://127.0.0.1 cancelled');
483-
})));
525+
$promise->then(null, $this->expectCallableOnceWith(
526+
$this->logicalAnd(
527+
$this->isInstanceOf('RuntimeException'),
528+
$this->callback(function (\RuntimeException $e) {
529+
return $e->getMessage() === 'Connection to mysql://127.0.0.1 cancelled (ECONNABORTED)';
530+
}),
531+
$this->callback(function (\RuntimeException $e) {
532+
return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103);
533+
})
534+
)
535+
));
484536
}
485537

486538
public function testCancelConnectDuringAuthenticationWillCloseConnection()
@@ -497,10 +549,17 @@ public function testCancelConnectDuringAuthenticationWillCloseConnection()
497549

498550
$promise->cancel();
499551

500-
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
501-
$promise->then(null, $this->expectCallableOnceWith($this->callback(function ($e) {
502-
return ($e->getMessage() === 'Connection to mysql://127.0.0.1 cancelled');
503-
})));
552+
$promise->then(null, $this->expectCallableOnceWith(
553+
$this->logicalAnd(
554+
$this->isInstanceOf('RuntimeException'),
555+
$this->callback(function (\RuntimeException $e) {
556+
return $e->getMessage() === 'Connection to mysql://127.0.0.1 cancelled (ECONNABORTED)';
557+
}),
558+
$this->callback(function (\RuntimeException $e) {
559+
return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103);
560+
})
561+
)
562+
));
504563
}
505564

506565
public function testConnectLazyWithAnyAuthWillQuitWithoutRunning()

0 commit comments

Comments
 (0)