getgrav/grav · error · InvalidArgumentException

Malformed URL: {$url}

Error message

Malformed URL: {$url}

What it means

UriFactory::parseUrl() percent-encodes multibyte characters, then hands the result to PHP's parse_url(); if parse_url() still returns false, the string is not a parseable URL at all and InvalidArgumentException ('Malformed URL: ...') is thrown with the offending input embedded. This catches strings that are structurally broken, not merely unusual.

Source

Thrown at system/src/Grav/Framework/Uri/UriFactory.php:134

     * @param string $url
     * @return array
     * @throws InvalidArgumentException
     */
    public static function parseUrl($url)
    {
        if (!is_string($url)) {
            throw new InvalidArgumentException('URL must be a string');
        }

        $encodedUrl = preg_replace_callback(
            '%[^:/@?&=#]+%u',
            static fn($matches) => rawurlencode((string) $matches[0]),
            $url
        );

        $parts = is_string($encodedUrl) ? parse_url($encodedUrl) : false;
        if ($parts === false) {
            throw new InvalidArgumentException("Malformed URL: {$url}");
        }

        return $parts;
    }

    /**
     * Parse query string and return it as an array.
     *
     * @param string $query
     * @return mixed
     */
    public static function parseQuery($query)
    {
        parse_str($query, $params);

        return $params;
    }

View on GitHub (pinned to 6040efed04)

Solutions

  1. Pre-validate with `filter_var($url, FILTER_VALIDATE_URL)` before parsing
  2. Reject empty/hostless strings early instead of relying on parse_url
  3. Wrap parseUrl()/createFromString() in try/catch and treat failure as invalid input (400), not a 500

Example fix

// before
$uri = UriFactory::createFromString($userUrl); // 'http://:80' -> Malformed URL

// after
if (!is_string($userUrl) || !filter_var($userUrl, FILTER_VALIDATE_URL)) {
    throw new HttpBadRequest('Invalid URL');
}
$uri = UriFactory::createFromString($userUrl);
Defensive patterns

Strategy: validation

Validate before calling

if (!is_string($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
    throw new \InvalidArgumentException("Not a valid URL: " . print_r($url, true));
}

Try / catch

try {
    $parts = UriFactory::parseUrl($url);
} catch (\InvalidArgumentException $e) {
    // 'Malformed URL: ...' — reject the input, do not retry
    return $response->withStatus(400)->write('Invalid URL');
}

Prevention

When it happens

Trigger: Strings like 'http://:80', '////', 'http://' with empty host, URLs containing raw control characters, or multibyte garbage that remains unparseable after encoding; feeding concatenated user input (e.g. `$base . $path` where $base is empty) into createFromString().

Common situations: User-submitted link/redirect fields; imported content containing broken URLs; crafted scanner requests hitting an endpoint that parses arbitrary input as a URL.

Understand the failure class

Related errors


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