Commit 100e8f353ea for php.net
commit 100e8f353ea8ef841339ba4c74a0966a7eea5157
Author: Máté Kocsis <kocsismate@woohoolabs.com>
Date: Tue Sep 8 21:33:21 2026 +0200
ext/uri: Preserve empty query and fragment in URL builders
Keep empty components distinct from null so serialization retains the trailing question mark or hashmark.
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_empty_string.phpt b/ext/uri/tests/whatwg/builder/fragment_success_empty_string.phpt
new file mode 100644
index 00000000000..2fe13ffd702
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/fragment_success_empty_string.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setFragment() - success - empty string
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("foo");
+$builder->setFragment("");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(21) "https://example.com/#"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ string(0) ""
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/query_success_empty_string.phpt b/ext/uri/tests/whatwg/builder/query_success_empty_string.phpt
new file mode 100644
index 00000000000..01cdb62a11c
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/query_success_empty_string.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setQuery() - success - empty string
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("https");
+$builder->setHost("example.com");
+$builder->setQuery("foo");
+$builder->setQuery("");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(21) "https://example.com/?"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ string(0) ""
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c
index b1bd2bcd865..0e5d9e8a66f 100644
--- a/ext/uri/uri_parser_whatwg.c
+++ b/ext/uri/uri_parser_whatwg.c
@@ -1068,13 +1068,27 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh
goto failure;
}
- result = php_uri_parser_whatwg_query_write(lexbor_url, query, NULL);
+ if (Z_TYPE_P(query) == IS_STRING && Z_STRLEN_P(query) == 0) {
+ /* The URL API setter treats an empty string as removal. The builder
+ * distinguishes an empty component from an absent one. */
+ lexbor_str_destroy(&lexbor_url->query, lexbor_url->mraw, false);
+ lexbor_str_init(&lexbor_url->query, lexbor_url->mraw, 1);
+ } else {
+ result = php_uri_parser_whatwg_query_write(lexbor_url, query, NULL);
+ }
php_uri_parser_whatwg_build_errors(&errors);
if (result == FAILURE) {
goto failure;
}
- result = php_uri_parser_whatwg_fragment_write(lexbor_url, fragment, NULL);
+ if (Z_TYPE_P(fragment) == IS_STRING && Z_STRLEN_P(fragment) == 0) {
+ /* The URL API setter treats an empty string as removal. The builder
+ * distinguishes an empty component from an absent one. */
+ lexbor_str_destroy(&lexbor_url->fragment, lexbor_url->mraw, false);
+ lexbor_str_init(&lexbor_url->fragment, lexbor_url->mraw, 1);
+ } else {
+ result = php_uri_parser_whatwg_fragment_write(lexbor_url, fragment, NULL);
+ }
php_uri_parser_whatwg_build_errors(&errors);
if (result == FAILURE) {
goto failure;