{"record":{"id":"188b41d73fe799da","repo":"thephpleague/flysystem","slug":"metadata-can-t-be-parsed-from-item-item-not-e","errorCode":null,"errorMessage":"Metadata can't be parsed from item '$item' , not enough parts.","messagePattern":"Metadata can't be parsed from item '\\$item' , not enough parts\\.","errorType":"exception","errorClass":"InvalidListResponseReceived","httpStatus":null,"severity":"error","filePath":"src/Ftp/FtpAdapter.php","lineNumber":408,"sourceCode":"\n        return $this->normalizeWindowsObject($item, $base);\n    }\n\n    private function detectSystemType(string $item): string\n    {\n        return preg_match(\n            '/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/',\n            $item\n        ) ? self::SYSTEM_TYPE_WINDOWS : self::SYSTEM_TYPE_UNIX;\n    }\n\n    private function normalizeWindowsObject(string $item, string $base): StorageAttributes\n    {\n        $item = preg_replace('#\\s+#', ' ', trim($item), 3);\n        $parts = explode(' ', $item, 4);\n\n        if (count($parts) !== 4) {\n            throw new InvalidListResponseReceived(\"Metadata can't be parsed from item '$item' , not enough parts.\");\n        }\n\n        [$date, $time, $size, $name] = $parts;\n        $path = $base === '' ? $name : rtrim($base, '/') . '/' . $name;\n\n        if ($size === '<DIR>') {\n            return new DirectoryAttributes($path);\n        }\n\n        // Check for the correct date/time format\n        $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';\n        $dt = DateTime::createFromFormat($format, $date . $time);\n        $lastModified = $dt ? $dt->getTimestamp() : (int) strtotime(\"$date $time\");\n\n        return new FileAttributes($path, (int) $size, null, $lastModified);\n    }\n\n    private function normalizeUnixObject(string $item, string $base): StorageAttributes","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/thephpleague/flysystem/blob/b277b5dc3d56650b68904117124e79c851e12376/src/Ftp/FtpAdapter.php#L390-L426","documentation":"When listing an FTP server detected as Windows-style (the LIST line starts with a date-like pattern such as MM-DD-YY or YYYY-MM-DD), FtpAdapter::normalizeWindowsObject() collapses whitespace and splits the line into exactly 4 parts: date, time, size-or-<DIR>, and name. Fewer than 4 parts means the line is not a parseable Windows directory entry, and InvalidListResponseReceived is thrown with the offending item embedded in the message.","triggerScenarios":"listContents() on an FTP server whose raw LIST output contains non-entry lines (messages like 'Total files: 42', banners, blank-ish lines) that got classified as Windows format; a server whose date format accidentally matches the Windows detection regex; localized or non-standard LIST formats.","commonSituations":"Connecting to Windows/IIS FTP servers with custom directory-listing formats; mainframe or appliance FTP gateways that prepend status lines; servers behind FTP proxies injecting text into listings; systemType left null so detection happens per-connection based on the first line seen.","solutions":["Pin the listing format explicitly: FtpConnectionOptions::lazy('host', ..., systemType: 'unix') or 'windows' — removing auto-detection when you know the server type.","Enable/disable 'recurseManually' or set 'useRawListContents' via a decorator to inspect raw LIST output and identify the offending line.","If the server offers MLSD (machine listings) prefer an MLSD-capable server config, or fix the server's LIST format / chroot so it emits standard lines.","Catch InvalidListResponseReceived around listContents() and log $e->getMessage() to capture the exact malformed item for the server admin."],"exampleFix":"// before (auto-detection misfires on odd first line)\n$options = FtpConnectionOptions::lazy('ftp.example.com');\n$adapter = new FtpAdapter($options);\n$adapter->listContents('/'); // throws \"Metadata can't be parsed from item 'Total files: 42' , not enough parts.\"\n\n// after (explicit system type)\n$options = FtpConnectionOptions::lazy('ftp.example.com', '/', FTP_NATIVE, 'windows'); // 5th arg = systemType\n$adapter = new FtpAdapter($options);","handlingStrategy":"validation","validationCode":"// Pin the server type instead of relying on per-connection auto-detection\nuse League\\Flysystem\\Ftp\\FtpConnectionOptions;\n\n$options = FtpConnectionOptions::lazy('ftp.example.com', '/', FTP_NATIVE, 'windows'); // explicit systemType\n$adapter = new FtpAdapter($options);\n\n// Optional: probe the raw listing before calling listContents\n$raw = ftp_rawlist($connection, '/');\nforeach ($raw as $line) {\n    if (substr_count(preg_replace('#\\s+#', ' ', trim($line)), ' ') < 3) {\n        // non-entry line detected: filter or abort before Flysystem parses it\n    }\n}","typeGuard":null,"tryCatchPattern":"use League\\Flysystem\\InvalidListResponseReceived;\n\ntry {\n    $contents = $filesystem->listContents('/')->toArray();\n} catch (InvalidListResponseReceived $e) {\n    // message contains the exact malformed line — log it for the server admin\n    $this->logger->error('Unparseable FTP listing line', ['exception' => $e]);\n    $contents = []; // or fall back to non-recursive per-file has() checks\n}","preventionTips":["Always set systemType explicitly in FtpConnectionOptions for known servers.","Smoke-test listContents() during deployment to catch listing-format changes after server upgrades.","Keep FTP servers on standard LIST output; avoid banners injected into directory listings."],"tags":["php","flysystem","ftp","list-contents","parsing","windows-ftp"],"backgroundTag":"ftp-listing-parse-failed","analyzedSha":"b277b5dc3d56650b68904117124e79c851e12376","analyzedAt":"2026-08-17T04:28:35.741Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}