From b7378e0cbe83be93ba60cf956a1042dc15725e34 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 27 Oct 2026 10:47:14 +0300 Subject: [PATCH] Fix "Undefined array key" error for stream resources without URI --- CHANGELOG.md | 2 ++ src/Transport/CurlTransport.php | 2 +- src/Type/InputFile.php | 8 +++-- tests/Support/TestHelper.php | 19 ++++++++++ .../CurlTransport/CurlTransportTest.php | 24 +++++++++++++ .../NativeTransport/NativeTransportTest.php | 35 +++++++++++++++++++ tests/Type/InputFileTest.php | 13 +++++++ 7 files changed, 100 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de1ea20a..029a3256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ a string, a `RichText` object or an array of them, instead of failing with `TypeError` or silently casting scalars to strings. - Bug #227: Discard keys of rich text arrays on serialization, so they are always sent as JSON arrays, not objects. +- Bug #228: Fix "Undefined array key" error in `InputFile::filename()` and `CurlTransport` when a stream resource has + no URI. ## 0.22 August 25, 2026 diff --git a/src/Transport/CurlTransport.php b/src/Transport/CurlTransport.php index 7eaea0e2..1349538e 100644 --- a/src/Transport/CurlTransport.php +++ b/src/Transport/CurlTransport.php @@ -85,7 +85,7 @@ private function toCurlFile(InputFile $file): CURLFile|CURLStringFile } $metadata = stream_get_meta_data($file->pathOrResource); - if (!str_contains($metadata['uri'], '://')) { + if (isset($metadata['uri']) && !str_contains($metadata['uri'], '://')) { return new CURLFile($metadata['uri'], $mimeType, $file->filename()); } diff --git a/src/Type/InputFile.php b/src/Type/InputFile.php index 94b9d57f..968e42cd 100644 --- a/src/Type/InputFile.php +++ b/src/Type/InputFile.php @@ -35,8 +35,12 @@ public function filename(): ?string return basename($this->pathOrResource); } - $uri = stream_get_meta_data($this->pathOrResource)['uri']; - return str_contains($uri, '://') ? null : basename($uri); + $uri = stream_get_meta_data($this->pathOrResource)['uri'] ?? null; + if ($uri === null || str_contains($uri, '://')) { + return null; + } + + return basename($uri); } /** diff --git a/tests/Support/TestHelper.php b/tests/Support/TestHelper.php index 65687dd9..6b540c33 100644 --- a/tests/Support/TestHelper.php +++ b/tests/Support/TestHelper.php @@ -5,6 +5,7 @@ namespace Phptg\BotApi\Tests\Support; use Psr\Log\LoggerInterface; +use RuntimeException; use Phptg\BotApi\Transport\ApiResponse; use Phptg\BotApi\TelegramBotApi; @@ -26,4 +27,22 @@ public static function createSuccessStubApi( logger: $logger, ); } + + /** + * Creates a pair of connected sockets. Such stream resources have no URI in their metadata. + * + * @return array{resource, resource} + */ + public static function createSocketPair(): array + { + $pair = stream_socket_pair( + PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, + STREAM_SOCK_STREAM, + STREAM_IPPROTO_IP, + ); + if ($pair === false) { + throw new RuntimeException('Failed to create a socket pair.'); + } + return $pair; + } } diff --git a/tests/Transport/CurlTransport/CurlTransportTest.php b/tests/Transport/CurlTransport/CurlTransportTest.php index e8e167c4..4df2c759 100644 --- a/tests/Transport/CurlTransport/CurlTransportTest.php +++ b/tests/Transport/CurlTransport/CurlTransportTest.php @@ -10,6 +10,7 @@ use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Phptg\BotApi\Tests\Curl\CurlMock; +use Phptg\BotApi\Tests\Support\TestHelper; use Phptg\BotApi\Transport\CurlTransport; use Phptg\BotApi\Type\InputFile; @@ -155,6 +156,29 @@ public function testPostWithResource(string $mimeType, ?string $filename): void assertSame($mimeType, $postFields['file']->mime); } + public function testPostWithResourceWithoutUri(): void + { + $curl = new CurlMock(); + $transport = new CurlTransport(curl: $curl); + + [$resource, $pair] = TestHelper::createSocketPair(); + fwrite($pair, 'socket content'); + fclose($pair); + + $transport->postWithFiles( + '//url/method', + [], + ['file' => new InputFile($resource)], + ); + + fclose($resource); + + $postFields = $curl->getOptions()[CURLOPT_POSTFIELDS]; + assertInstanceOf(CURLStringFile::class, $postFields['file']); + assertSame('socket content', $postFields['file']->data); + assertSame('', $postFields['file']->postname); + } + public function testSeekableResource(): void { $curl = new CurlMock(); diff --git a/tests/Transport/NativeTransport/NativeTransportTest.php b/tests/Transport/NativeTransport/NativeTransportTest.php index c1a670b2..b9ac9d48 100644 --- a/tests/Transport/NativeTransport/NativeTransportTest.php +++ b/tests/Transport/NativeTransport/NativeTransportTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use RuntimeException; +use Phptg\BotApi\Tests\Support\TestHelper; use Phptg\BotApi\Tests\Transport\NativeTransport\StreamMock\StreamMock; use Phptg\BotApi\Transport\MimeTypeResolver\MimeTypeResolverInterface; use Phptg\BotApi\Transport\NativeTransport; @@ -269,6 +270,40 @@ public function testPostWithSeekableStreamResource(): void assertStringContainsString('stream content', $request['options']['http']['content']); } + public function testPostWithResourceWithoutUri(): void + { + $transport = new NativeTransport(); + + StreamMock::enable( + responseHeaders: [ + 'HTTP/1.1 200 OK', + 'Content-Type: text/json', + ], + responseBody: '{"ok":true,"result":[]}', + ); + + [$resource, $pair] = TestHelper::createSocketPair(); + fwrite($pair, 'socket content'); + fclose($pair); + + $transport->postWithFiles( + 'http://url/method', + [], + ['file' => new InputFile($resource)], + ); + + fclose($resource); + + $request = StreamMock::disable(); + + assertStringContainsString( + "Content-Disposition: form-data; name=\"file\"\r\n" + . "\r\n" + . "socket content\r\n", + $request['options']['http']['content'], + ); + } + public function testErrorOnSend(): void { $transport = new NativeTransport(); diff --git a/tests/Type/InputFileTest.php b/tests/Type/InputFileTest.php index cfda24a6..3b2659a8 100644 --- a/tests/Type/InputFileTest.php +++ b/tests/Type/InputFileTest.php @@ -5,6 +5,7 @@ namespace Phptg\BotApi\Tests\Type; use PHPUnit\Framework\TestCase; +use Phptg\BotApi\Tests\Support\TestHelper; use Phptg\BotApi\Type\InputFile; use function PHPUnit\Framework\assertNull; @@ -53,6 +54,18 @@ public function testFromVirtualResource(): void fclose($resource); } + public function testFromResourceWithoutUri(): void + { + [$resource, $pair] = TestHelper::createSocketPair(); + $file = new InputFile($resource); + + assertNull($file->filename()); + assertNull($file->extension()); + + fclose($resource); + fclose($pair); + } + public function testFromResourceWithFilename(): void { $resource = fopen('php://temp', 'r+b');