Skip to content

Commit 5d850d7

Browse files
committed
bump: version 0.1.1
Move curl exit code descriptions from ProcessTransport into NetworkException::fromCurlExitCode() factory method.
1 parent f0d7f80 commit 5d850d7

4 files changed

Lines changed: 82 additions & 3 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "PHP TLS/HTTP fingerprinting library for browser emulation",
44
"keywords": ["php", "tls", "fingerprint", "http2", "curl-impersonate", "web-scraping", "browser-emulation"],
55
"license": "MIT",
6-
"version": "0.1.0",
6+
"version": "0.1.1",
77
"authors": [
88
{
99
"name": "Daniel Reis",

src/Exception/NetworkException.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,61 @@ public function __construct(
1818
parent::__construct($message, $code, $previous);
1919
}
2020

21+
/**
22+
* Create from a curl_impersonate process failure.
23+
*/
24+
public static function fromCurlExitCode(RequestInterface $request, int $exitCode, string $stderr = ''): self
25+
{
26+
$detail = $stderr !== '' ? $stderr : self::describeExitCode($exitCode);
27+
28+
return new self(
29+
$request,
30+
'curl_impersonate failed (exit '.$exitCode.'): '.$detail,
31+
$exitCode,
32+
);
33+
}
34+
2135
public function getRequest(): RequestInterface
2236
{
2337
return $this->request;
2438
}
39+
40+
/**
41+
* Map libcurl exit codes to human-readable descriptions.
42+
*
43+
* @see https://curl.se/libcurl/c/libcurl-errors.html
44+
*/
45+
private static function describeExitCode(int $code): string
46+
{
47+
return match ($code) {
48+
1 => 'unsupported protocol',
49+
2 => 'failed to initialize',
50+
3 => 'malformed URL',
51+
5 => 'could not resolve proxy',
52+
6 => 'could not resolve host',
53+
7 => 'connection refused',
54+
9 => 'access denied (login/credentials)',
55+
18 => 'partial transfer (connection closed prematurely)',
56+
22 => 'HTTP error (server returned >= 400)',
57+
23 => 'write error (disk full or permissions)',
58+
26 => 'read error (could not read local file)',
59+
27 => 'out of memory',
60+
28 => 'operation timed out',
61+
33 => 'range error (server does not support byte ranges)',
62+
35 => 'SSL/TLS handshake failed',
63+
47 => 'too many redirects',
64+
51 => 'SSL certificate verification failed (peer)',
65+
52 => 'empty reply from server',
66+
55 => 'send error (network failure)',
67+
56 => 'receive error (connection reset)',
68+
58 => 'SSL client certificate error',
69+
60 => 'SSL CA certificate not found or not trusted',
70+
67 => 'login denied (authentication failure)',
71+
77 => 'SSL CA certificate path error',
72+
92 => 'HTTP/2 stream error',
73+
95 => 'HTTP/2 error',
74+
97 => 'HTTP/3 error',
75+
default => 'unknown error (code '.$code.')',
76+
};
77+
}
2578
}

src/Transport/ProcessTransport.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ public function send(RequestInterface $request, Profile $profile, TransportOptio
5858
$exitCode = proc_close($process);
5959

6060
if ($exitCode !== 0 || $stdout === false) {
61-
throw new NetworkException(
61+
throw NetworkException::fromCurlExitCode(
6262
$request,
63-
'curl_impersonate failed (exit '.$exitCode.'): '.($stderr !== false && $stderr !== '' ? $stderr : 'unknown error'),
6463
$exitCode,
64+
$stderr !== false && $stderr !== '' ? trim($stderr) : '',
6565
);
6666
}
6767

tests/Unit/Exception/NetworkExceptionTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,29 @@
5151
expect($exception->getMessage())->toBe('')
5252
->and($exception->getCode())->toBe(0);
5353
});
54+
55+
it('creates from curl exit code with stderr detail', function (): void {
56+
$request = new Request('GET', 'https://example.com');
57+
$exception = NetworkException::fromCurlExitCode($request, 7, 'Connection refused');
58+
59+
expect($exception)->toBeInstanceOf(NetworkException::class)
60+
->and($exception->getMessage())->toBe('curl_impersonate failed (exit 7): Connection refused')
61+
->and($exception->getCode())->toBe(7)
62+
->and($exception->getRequest())->toBe($request);
63+
});
64+
65+
it('creates from curl exit code with mapped description when stderr is empty', function (): void {
66+
$request = new Request('GET', 'https://example.com');
67+
$exception = NetworkException::fromCurlExitCode($request, 28);
68+
69+
expect($exception->getMessage())->toBe('curl_impersonate failed (exit 28): operation timed out')
70+
->and($exception->getCode())->toBe(28);
71+
});
72+
73+
it('creates from curl exit code with unknown code fallback', function (): void {
74+
$request = new Request('GET', 'https://example.com');
75+
$exception = NetworkException::fromCurlExitCode($request, 999);
76+
77+
expect($exception->getMessage())->toBe('curl_impersonate failed (exit 999): unknown error (code 999)')
78+
->and($exception->getCode())->toBe(999);
79+
});

0 commit comments

Comments
 (0)