Skip to content

Stop inheriting the query and fragment of the base URI - #951

Open
Amoifr wants to merge 2 commits into
jsonrainbow:mainfrom
Amoifr:fix-948-uri-resolver-query-fragment
Open

Stop inheriting the query and fragment of the base URI#951
Amoifr wants to merge 2 commits into
jsonrainbow:mainfrom
Amoifr:fix-948-uri-resolver-query-fragment

Conversation

@Amoifr

@Amoifr Amoifr commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #948.

resolve() reused the parsed base components wholesale, so the base query and fragment survived into the resolved URI.

While reproducing it I found a second half you did not mention: the query of the reference was dropped entirely, not just the base one kept.

$resolver->resolve('bar.json?new', 'http://example.org/foo/x.json?old#frag');
// before: http://example.org/foo/bar.json?old#frag
// after:  http://example.org/foo/bar.json?new

So the rule now follows RFC 3986 section 5.3: a reference carrying a path replaces the query of the base whether or not it has one of its own, only a reference without a path keeps it, and the fragment always comes from the reference.

One detail worth flagging, because it is the trap here. isset($components['query']) is not enough: parse('#/definitions/x') reports an empty query, so testing the key alone would drop the base query for every bare fragment, which is the most common $ref shape in this library. The condition therefore tests for a non-empty value, which is consistent with generate() already omitting an empty query.

Testing

Six cases in a data provider. Five of them fail on main, the sixth is the bare fragment against a base with a query, which already worked and is there to keep it working.

Full suite green (3187 tests), same warning and skips as main, and PHPStan reports no errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WUCMYykYqJHv67fmwiWZip

Copilot AI 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.

🟡 Changes recommended

Empty references and explicit empty queries still incorrectly inherit base URI components.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates URI resolution to follow RFC 3986 query and fragment inheritance rules.

Changes:

  • Clears inherited queries for path-bearing references.
  • Ensures fragments come from the reference.
  • Adds query and fragment resolution tests.
File summaries
File Description
src/JsonSchema/Uri/UriResolver.php Revises query and fragment resolution.
tests/Uri/UriResolverTest.php Adds regression cases for URI inheritance.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/JsonSchema/Uri/UriResolver.php Outdated
// RFC 3986 section 5.3: a reference carrying a path replaces the query of the base,
// whether or not it has one of its own. Only a reference without a path, such as a
// bare fragment, keeps it.
if ('' !== $path) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct, and I measured it before changing anything:

resolve('?', 'http://e.org/a/x.json?old')  =  http://e.org/a/x.json?old

RFC 3986 section 5.2.2 treats a query that is supplied but empty as defined, so it replaces the base one rather than letting it through.

One thing your suggestion did not say but which decides the shape of the fix: parse() cannot tell an empty query from an absent one, because both come out as ''.

parse('#f') => path='' query=''
parse('?')  => path='' query=''

So the components are no help here and the delimiter has to be looked for in the reference itself, which is why e0f078c does

$hasQuery = false !== strpos(explode('#', $uri, 2)[0], '?');

rather than testing $components['query']. ? and ?#f are both in the provider now.

$baseComponents['query'] = $components['query'];
}

// the fragment always comes from the reference and is never inherited

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct as well:

resolve('', 'http://e.org/a/x.json?old#frag')  =  http://e.org/a/x.json?old#frag

RFC 3986 assigns T.fragment = R.fragment with no exception, so an empty reference takes the absent fragment of the reference and keeps only the path and query of the base. My own comment two lines below claimed that invariant while the early return quietly bypassed it.

e0f078c restricts the early return to the case where there is no base to resolve against ('' === $uri && (null === $baseUri || '' === $baseUri)), so everything else goes through component resolution. testResolveEmpty keeps its original case and a second one covers the fragment-bearing base.

Both changes are pinned by tests that fail without them. Full suite green: 3190 tests, and the single remaining warning is on the base branch too.

RFC 3986 section 5.2.2 treats a query that is supplied but empty as defined, so
it replaces the query of the base rather than letting it through, and it takes
the fragment from the reference without exception, so an empty reference must
not keep the fragment of its base.

parse() cannot tell an empty query from an absent one, both being '', so the
delimiter is looked for in the reference itself. The early return for an empty
reference now applies only when there is no base to resolve against.
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.

UriResolver::resolve() inherits the base URI query and fragment

2 participants