getgrav/grav · error · RuntimeException

Malformed GPM URL: {$package_file}

Error message

Malformed GPM URL: {$package_file}

What it means

GPM::downloadPackage() (line 720) runs the supplied package URL through parse_url(); when parse_url cannot return an array (i.e. returns false for a seriously malformed URL), Grav throws before making any HTTP request. This guards the package installer against garbage input; note that a mere missing scheme (e.g. 'plugin.zip') still parses and fails later checks instead.

Source

Thrown at system/src/Grav/Common/GPM/GPM.php:720

                return $plugin;
            }
        }

        return false;
    }

    /**
     * Download the zip package via the URL
     *
     * @param string $package_file
     * @param string $tmp
     * @return string|null
     */
    public static function downloadPackage($package_file, $tmp)
    {
        $package = parse_url($package_file);
        if (!is_array($package)) {
            throw new \RuntimeException("Malformed GPM URL: {$package_file}");
        }

        $filename = Utils::basename($package['path'] ?? '');

        if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && ($package['host'] ?? null) !== 'getgrav.org') {
            throw new RuntimeException('Only official GPM URLs are allowed. You can modify this behavior in the System configuration.');
        }

        $output = Response::get($package_file, []);

        if ($output) {
            Folder::create($tmp);
            file_put_contents($tmp . DS . $filename, $output);
            return $tmp . DS . $filename;
        }

        return null;
    }

View on GitHub (pinned to 6040efed04)

Solutions

  1. Validate the input first: filter_var($url, FILTER_VALIDATE_URL) and reject non-URLs with a friendly message before calling downloadPackage.
  2. Fix the URL itself — it must be a parseable absolute URL with scheme and host (e.g. https://getgrav.org/...).
  3. If you meant a local file, install it via the local path/extraction route (Installer with an extracted source) rather than the GPM downloader.

Example fix

// before
GPM::downloadPackage($input, $tmp); // RuntimeException: Malformed GPM URL

// after
if (!filter_var($input, FILTER_VALIDATE_URL)) {
    throw new \InvalidArgumentException('Please provide a valid package URL.');
}
GPM::downloadPackage($input, $tmp);
Defensive patterns

Strategy: validation

Validate before calling

if (!\is_string($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
    throw new \InvalidArgumentException('A valid absolute package URL is required.');
}
GPM::downloadPackage($url, $tmp);

Type guard

function isDownloadablePackageUrl(mixed $url): bool
{
    return \is_string($url) && (bool) filter_var($url, FILTER_VALIDATE_URL)
        && \is_array(parse_url($url));
}

Try / catch

try {
    $path = GPM::downloadPackage($url, $tmp);
} catch (\RuntimeException $e) {
    // report 'invalid package URL' to the user; do not retry the same string
}

Prevention

When it happens

Trigger: Calling GPM::downloadPackage($package_file, $tmp) with an unparseable URL such as 'http://:80', a string containing control characters, or a raw filename where an absolute URL is expected; install-from-URL flows that pass user/plugin-provided values through without validation.

Common situations: Custom admin tools offering 'install from URL' that forward raw input; copy-pasted URLs with invisible whitespace; code that used to pass local paths and broke after the API started requiring URLs.

Understand the failure class

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/9afdca7a0401a6c0. Report an issue: GitHub.