Skip to content

Use the request's API version in URL mode when the connector has one - #567

Open
Cbrad24 wants to merge 1 commit into
saloonphp:v4from
Cbrad24:fix/api-version-url-request-override
Open

Cbrad24 wants to merge 1 commit into
saloonphp:v4from
Cbrad24:fix/api-version-url-request-override

Conversation

@Cbrad24

@Cbrad24 Cbrad24 commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

PR #564 introduces a HasApiVersion trait for API versioning, including a {version} placeholder in the URL with VersionMode::Url.

I tried to use this with Microsoft Graph's API, as its a good fit for switching between Graph's v1.0 and beta APIs. The problem I ran into was that with VersionMode::Url, the connector's version always wins when it has one, and $request->setApiVersion() is ignored.

This is because the connector boots first and replaces {version} on its pass, so when the request's plugin boots there is no {version} left to replace:

  • $connector->send($request) creates a PendingRequest
  • The connector plugin boots and replaces {version}.
  • The request plugin boots and finds nothing to replace.
  • The override on the request does nothing.

This PR adds a getRequestApiVersion method that tries the request's version before the connector's, so a request wins, as #564's docs describe: "Prefer the request override when only one call should differ." It also adds a test that asserts a request override wins over a connector version.

Only VersionMode::Url is affected by the boot order, since VersionMode::Header and VersionMode::QueryParam both use add() which replaces the connector's value under the same key.

Example failing code:

class GraphConnector extends Connector
{
    use HasApiVersion;

    protected ?string $apiVersion = 'v1.0';
    protected VersionMode $versionMode = VersionMode::Url;

    public function resolveBaseUrl(): string
    {
        return 'https://graph.microsoft.com/{version}';
    }
}

$connector->send((new ListSignInsRequest)->setApiVersion('beta'));
// Sends https://graph.microsoft.com/v1.0/auditLogs/signIns, not /beta/

Side note: #564's description lists UrlPath and Subdomain as enum cases, but the merged enum has a single Url case covering both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@JonPurvis JonPurvis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @Cbrad24 馃殌

The connector boots HasApiVersion before the request, so the connector replaces {version} first and the request never gets a chance.

I've left a few comment but nothing too major. If those could be addressed, that'd be great. Regarding a point you made in your PR:

Side note: #564 description lists UrlPath and Subdomain as enum cases, but the merged enum has a single Url case covering both.

This was a very late change made pre merge because UrlPath and Subdomain both involved replacing part of the URL, so we decided to just make one Url case to handle both.

VersionMode::QueryParam => $pendingRequest->query()->add($this->versionKey, $this->apiVersion),
VersionMode::Url => $pendingRequest->setUrl(
str_replace('{version}', $this->ensureVersionIsSafeForUrl($this->apiVersion), $pendingRequest->getUrl())
str_replace('{version}', $this->ensureVersionIsSafeForUrl($this->getRequestApiVersion($pendingRequest) ?? $this->apiVersion), $pendingRequest->getUrl())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small readability tweak: call a resolveUrlApiVersion($pendingRequest) helper here (see my next comment for possible implementation). Same behaviour you鈥檙e going for, we still substitute the request鈥檚 version when the connector boots first, just not inlined as getRequestApiVersion() ?? $this->apiVersion.

Comment on lines +72 to +77
protected function getRequestApiVersion(PendingRequest $pendingRequest): ?string
{
$request = $pendingRequest->getRequest();

return method_exists($request, 'getApiVersion') ? $request->getApiVersion() : null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please could you rename this to resolveUrlApiVersion() so it鈥檚 more obvious it鈥檚 for the {version} placeholder, not header/query.

Couple of other suggestions too:

Only look at the request when the connector is booting ($pendingRequest->getConnector() === $this). When the request boots URL mode later, $this is already the request, so we can just use $this->apiVersion.

Only use the request鈥檚 version if the request is also in VersionMode::Url (needs a small getVersionMode() getter on the trait). If someone calls setApiVersion('beta') on a request that鈥檚 still in default Header mode, we should not change the URL path, only the header.

Detect HasApiVersion with Helpers::classUsesRecursive($request) instead of method_exists($request, 'getApiVersion'), so unrelated getApiVersion() methods don鈥檛 affect URLs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

/**
 * Resolve which version to substitute into `{version}` for URL mode.
 */
protected function resolveUrlApiVersion(PendingRequest $pendingRequest): string
{
    if ($pendingRequest->getConnector() === $this) {
        $request = $pendingRequest->getRequest();

        if (in_array(self::class, Helpers::classUsesRecursive($request), true)
            && $request->getVersionMode() === VersionMode::Url) {
            $requestVersion = $request->getApiVersion();

            if (is_string($requestVersion) && $requestVersion !== '') {
                return $requestVersion;
            }
        }
    }

    return $this->apiVersion;
}

Then a getter:

public function getVersionMode(): VersionMode
{
    return $this->versionMode;
}


return true;
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test for this scenario?

Connector still uses VersionMode::Url with v1, but the request only overrides via header, makeVersionedRequest('beta', VersionMode::Header). Here setApiVersion('beta') must not change the URL; the path should stay https://api.provider.com/v1, and the header should be api-version: beta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants