Skip to content

Implement "Followup improvements for ext/uri" RFC - URL building with base URL - #23526

Draft
kocsismate wants to merge 2 commits into
php:masterfrom
kocsismate:uri-followup5
Draft

Implement "Followup improvements for ext/uri" RFC - URL building with base URL#23526
kocsismate wants to merge 2 commits into
php:masterfrom
kocsismate:uri-followup5

Conversation

@kocsismate

Copy link
Copy Markdown
Member

RFC: https://wiki.php.net/rfc/uri_followup#uri_building

Add support for passing a non-null $baseUrl parameter for Uri\WhatWg\UrlBuilder::build().

This PR is not complete yet (tests are missing).

}
}

ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_resolve_reference_from_zval(

@kocsismate kocsismate Aug 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Overall, this code below is pretty much hacky, works coincidentally, and to be honest I wish it wouldn't exist. 😅 On the other hand, it gets the job done! So thank you @arnaud-lb for the suggestion, your idea works indeed :) I would have never thought about it.

Due to the above mentioned implementation difficulties though, IMO we should prioritize if the feature is worth more or the sanity of our code ^^

@kocsismate
kocsismate requested a review from TimWolla August 31, 2026 22:02
Comment thread ext/uri/tests/whatwg/builder/basic_error_with_opaque_base.phpt Outdated
Comment on lines +23 to +25
string(4) "user"
["password"]=>
string(4) "pass"

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.

Is this right? In #22268 (comment) you mention parsing rules. Resolving //example.net:124/foo/bar/baz drops user:pass. Are they here intentionally preserved? How is the builder generally supposed to work? From the RFC, your comments, and this test here it's not really clear to me what's actually intended behaviour. Like, generally, should this resolve the builder components against the base like parsing or replace the defined components of the base and keep the rest?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for catching this. This was due to a small oversight in the implementation: the username and password should have been removed. it's going to be fixed with my new push.

@NickSdot

NickSdot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

@kocsismate is the following expected?

### Rfc3986
$builder = new Uri\Rfc3986\UriBuilder();
$builder->setPort(123);

try {
    var_dump($builder->build(new Uri\Rfc3986\Uri("https://example.com"))->toAsciiString());
} catch (Throwable $e) {
    echo $e::class, ': ', $e->getMessage(), "\n";
}

// Uri\InvalidUriException: Cannot set a port without having a host


### WhatWg
$builder = new Uri\WhatWg\UrlBuilder();
$builder->setPort(123);

try {
    var_dump($builder->build(new Uri\WhatWg\Url("https://example.com"))->toAsciiString());
} catch (Throwable $e) {
    echo $e::class, ': ', $e->getMessage(), "\n";
}

// string(24) "https://example.com:123/"

I would expect RFC3986 to behave like WHATWG here. While trying to come up with tests for here and some more for #23342 I got stuck one way or another. The follow up RFC does not really spell out what the merging behaviour should be. The discussion and comments point towards resolution, but that also does not seem to be how everything consistently works?

For me it seems like WHATWG behaves sanely in the example above and RFC3986 does not, but then there are situations where WHATWG seems wrong too: A) see above; B) setting a fragment drops the query; C) fragments are appended instead of replaced; D) setting a username without a host results in https://example.comnew/a; and more.

Possible that I am having a dumb day, but I cannot really make sense of what the expected behaviour is. Could you please clarify how both RFC3986 and WHATWG are supposed to merge with a base URI/URL? Must the builder components form a valid reference on their own before resolving or can the base provide missing parts like the host?

Would love to add some tests to lock in the findings, but I really could use a hint first.

… base URL

RFC: https://wiki.php.net/rfc/uri_followup#uri_building

Add support for passing a non-null $baseUrl parameter for Uri\WhatWg\UrlBuilder::build().
@kocsismate

kocsismate commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

I would expect RFC3986 to behave like WHATWG here. While trying to come up with tests for here and some more for #23342 I got stuck one way or another. The follow up RFC does not really spell out what the merging behaviour should be. The discussion and comments point towards resolution, but that also does not seem to be how everything consistently works?

Ah no, it should be the other way around, and this was also due to a small ordering issue in the builder's code.

The merging behavior should be the very same what is done during parsing. E.g.

$builder = new Uri\WhatWg\UrlBuilder();
$builder->set*(...);
...
$url1 = $builder->build($baseUrl);

$url2 = new Uri\WhatWg\Url("...", $baseUrl);

For each $baseUrl, one of the two must be true:

  • $url1 and $url2 are both invalid with the same reason
  • $url1 and $url2 are equivalent

(Given that the resulting $url1 after the set*() calls would yield the same URL string as $url2)

The same goes for RFC 3986.

A) #23526 (comment);

Answered above.

B) setting a fragment drops the query;

Can you please elaborate upon this?

C) fragments are appended instead of replaced

Ah, this is an "annoying" behavior of the WHATWG URL spec (because our use-case is not really supported by the spec). That's why I had to add e.g. lxb_url_path_set_null(lexbor_url); and similar calls, but I forgot to do it with the query and the fragment.

D) setting a username without a host results in https://example.comnew/a; and more.

This is the same category as A, and it's fixed with the code reordering.

@NickSdot

NickSdot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Ah no, it should be the other way around, and this was also due to a small ordering issue in the builder's code.

The merging behavior should be the very same what is done during parsing. E.g.

$builder = new Uri\WhatWg\UrlBuilder();
$builder->set*(...);
...
$url1 = $builder->build($baseUrl);

$url2 = new Uri\WhatWg\Url("...", $baseUrl);

For each $baseUrl, one of the two must be true:

  • $url1 and $url2 are both invalid with the same reason
  • $url1 and $url2 are equivalent

I see why it is. But it also makes it very restrictive and rather surprising for "normal" usage. Like, not being able to append a port is kinda unfortunate. But fair enough.

Having some way to have "component overlays" would be neat. Like, how I expected it - just to replace/append/remove from a given URL. Probably that's something for the future, maybe UrlBuilder::fromUrl(). Different topic...

Can you please elaborate upon this?

I believe it was:

$base = https://example.com/a?old"
$builder->setFragment("new");
$builder->build($base); // https://example.com/a#new

But I will need to double check tomorrow.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants