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
- Validate the input first: filter_var($url, FILTER_VALIDATE_URL) and reject non-URLs with a friendly message before calling downloadPackage.
- Fix the URL itself — it must be a parseable absolute URL with scheme and host (e.g. https://getgrav.org/...).
- 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
- Trim and validate URLs at the input boundary of any install-from-URL feature.
- Reject schemeless strings early instead of relying on downstream GPM checks.
- Add UI-side pattern hints so users paste full https:// URLs.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Bad package file: %s
- Theme name not provided.
- Uploaded archive file is not a valid Grav update package
- Uploaded archive file is not a valid Grav update package
- Failed to save file '%s': %s
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/9afdca7a0401a6c0.
Report an issue: GitHub.