Skip to content

Guzzle MalformedUriException on Subscription getLink('project') calls #99

@coolbrewed

Description

@coolbrewed

Issue

When calling a function like getProject() or getOwner() on a Subscription object, the call fails with the following error:

GuzzleHttp\Psr7\Exception\MalformedUriException: A relative URI must not have a path beginning with a segment containing a colon in /app/vendor/guzzlehttp/psr7/src/Uri.php:741

Stack trace:

#0 /app/vendor/guzzlehttp/psr7/src/Uri.php(498): GuzzleHttp\Psr7\Uri->validateState()
#1 /app/vendor/platformsh/client/src/Model/ApiResourceBase.php(580): GuzzleHttp\Psr7\Uri->withPath('https://us.plat...')
#2 /app/vendor/platformsh/client/src/Model/ApiResourceBase.php(442): Platformsh\Client\Model\ApiResourceBase->makeAbsoluteUrl('https://us.plat...')
#3 /app/vendor/platformsh/client/src/Model/Subscription.php(171): Platformsh\Client\Model\ApiResourceBase->getLink('project', false)
#4 /app/vendor/platformsh/client/src/Model/Subscription.php(142): Platformsh\Client\Model\Subscription->getLink('project')
#5 /app/public/index.php(41): Platformsh\Client\Model\Subscription->getProject()

Cause

The Subscription's project link is already a complete URL rather than relative URL, e.g.: https://us.platform.sh/api/projects/PROJECT_ID instead of /api/projects/PROJECT_ID

When the logic reaches the makeAbsoluteUrl() function of the client ApiResourceBase class, it tries to pass the project's absolute URL as a relative path for the $target value, resulting in something that looks like: $base->withPath("https://us.platform.sh/api/projects/PROJECT_ID")

As a result, Guzzle's URI validation rejects it as not a valid path.

$base object inspection:

GuzzleHttp\Psr7\Uri Object
(
    [scheme:GuzzleHttp\Psr7\Uri:private] => 
    [userInfo:GuzzleHttp\Psr7\Uri:private] => 
    [host:GuzzleHttp\Psr7\Uri:private] => 
    [port:GuzzleHttp\Psr7\Uri:private] => 
    [path:GuzzleHttp\Psr7\Uri:private] => /organizations/ORG_ID/subscriptions
    [query:GuzzleHttp\Psr7\Uri:private] => 
    [fragment:GuzzleHttp\Psr7\Uri:private] => 
    [composedComponents:GuzzleHttp\Psr7\Uri:private] => 
)

$target object inspection:

GuzzleHttp\Psr7\Uri Object
(
    [scheme:GuzzleHttp\Psr7\Uri:private] => https
    [userInfo:GuzzleHttp\Psr7\Uri:private] => 
    [host:GuzzleHttp\Psr7\Uri:private] => us.platform.sh
    [port:GuzzleHttp\Psr7\Uri:private] => 
    [path:GuzzleHttp\Psr7\Uri:private] => /api/projects/PROJECT_ID
    [query:GuzzleHttp\Psr7\Uri:private] => 
    [fragment:GuzzleHttp\Psr7\Uri:private] => 
    [composedComponents:GuzzleHttp\Psr7\Uri:private] => 
)

Possible fix

Based on the current logic, it seemed like the base URI would always be used (whether a full URL or just a path itself), with the target path providing the new path value.

With some light/specific testing, I have gotten this to behave:

protected function makeAbsoluteUrl(string $relativeUrl, string $baseUrl = null): string
    {
        $baseUrl = $baseUrl ?: $this->baseUrl;
        if (empty($baseUrl)) {
            throw new \RuntimeException('No base URL');
        }
        $base = Utils::uriFor($baseUrl);
        $target = Utils::uriFor($relativeUrl);

        $url = $base->withPath($target->getPath());
        
        return (string) $url;
    }

Workaround

In the meantime, calling $client->getProject($projectId) instead of $subscription->getProject() works totally fine!

$projectId = $subscription->project_id;
$project = $client->getProject($projectId);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions