Commit a7a2303f957 for php.net

commit a7a2303f957c375ecf34bebdcfd319331e1dc76d
Author: Máté Kocsis <kocsismate@woohoolabs.com>
Date:   Tue Sep 8 21:32:47 2026 +0200

    ext/uri: Return builder soft errors consistently

    Rename the output argument to softErrors and clear it on error-free success. Preserve it on failure while including earlier soft errors in the exception. This makes the behavior consistent with parsing in the constructor.

diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c
index 457e123bb48..9d6e58dc181 100644
--- a/ext/uri/php_uri.c
+++ b/ext/uri/php_uri.c
@@ -1404,12 +1404,12 @@ PHP_METHOD(Uri_WhatWg_UrlBuilder, setFragment)
 PHP_METHOD(Uri_WhatWg_UrlBuilder, build)
 {
 	zval *base_url_zv = NULL;
-	zval *errors = NULL;
+	zval *soft_errors = NULL;

 	ZEND_PARSE_PARAMETERS_START(0, 2)
 		Z_PARAM_OPTIONAL
 		Z_PARAM_OBJECT_OF_CLASS_OR_NULL(base_url_zv, php_uri_ce_whatwg_url)
-		Z_PARAM_ZVAL(errors)
+		Z_PARAM_ZVAL(soft_errors)
 	ZEND_PARSE_PARAMETERS_END();

 	const zval *scheme = Z_WHATWG_URL_PROP_SCHEME_DEREF_P(ZEND_THIS);
@@ -1430,7 +1430,7 @@ PHP_METHOD(Uri_WhatWg_UrlBuilder, build)

 	lxb_url_t *lexbor_url = php_uri_parser_whatwg_build_from_zval(
 		base_url, scheme, username, password, host, port, path, query, fragment,
-		errors
+		soft_errors
 	);
 	if (lexbor_url == NULL) {
 		RETURN_THROWS();
diff --git a/ext/uri/php_uri.stub.php b/ext/uri/php_uri.stub.php
index ad1d2fe32de..feca1871cea 100644
--- a/ext/uri/php_uri.stub.php
+++ b/ext/uri/php_uri.stub.php
@@ -240,8 +240,8 @@ public function setQuery(?string $query): static {}

         public function setFragment(?string $fragment): static {}

-        /** @param array $errors */
-        public function build(?\Uri\WhatWg\Url $baseUrl = null, &$errors = null): \Uri\WhatWg\Url {}
+        /** @param array $softErrors */
+        public function build(?\Uri\WhatWg\Url $baseUrl = null, &$softErrors = null): \Uri\WhatWg\Url {}
     }

     /** @strict-properties */
diff --git a/ext/uri/php_uri_arginfo.h b/ext/uri/php_uri_arginfo.h
index b9b4fedc10c..8139d4fbf7b 100644
Binary files a/ext/uri/php_uri_arginfo.h and b/ext/uri/php_uri_arginfo.h differ
diff --git a/ext/uri/php_uri_decl.h b/ext/uri/php_uri_decl.h
index a55b44a9520..04d12097833 100644
Binary files a/ext/uri/php_uri_decl.h and b/ext/uri/php_uri_decl.h differ
diff --git a/ext/uri/tests/whatwg/builder/build_error_soft_errors_unchanged.phpt b/ext/uri/tests/whatwg/builder/build_error_soft_errors_unchanged.phpt
new file mode 100644
index 00000000000..5cba13e51c1
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/build_error_soft_errors_unchanged.phpt
@@ -0,0 +1,46 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::build() - error - leaves soft errors unchanged
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("ht\ttps");
+$builder->setHost(null);
+$softErrors = ["unchanged"];
+
+try {
+    $builder->build(softErrors: $softErrors);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+    var_dump($e->errors);
+}
+
+var_dump($softErrors);
+
+?>
+--EXPECTF--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (HostMissing)
+array(2) {
+  [0]=>
+  object(Uri\WhatWg\UrlValidationError)#%d (%d) {
+    ["context"]=>
+    string(4) "	tps"
+    ["type"]=>
+    enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit)
+    ["failure"]=>
+    bool(false)
+  }
+  [1]=>
+  object(Uri\WhatWg\UrlValidationError)#%d (%d) {
+    ["context"]=>
+    string(0) ""
+    ["type"]=>
+    enum(Uri\WhatWg\UrlValidationErrorType::HostMissing)
+    ["failure"]=>
+    bool(true)
+  }
+}
+array(1) {
+  [0]=>
+  string(9) "unchanged"
+}
diff --git a/ext/uri/tests/whatwg/builder/build_success_soft_errors.phpt b/ext/uri/tests/whatwg/builder/build_success_soft_errors.phpt
new file mode 100644
index 00000000000..b2a860f03f5
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/build_success_soft_errors.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::build() - success - returns soft errors
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("a\tb");
+$softErrors = [];
+$url = $builder->build(softErrors: $softErrors);
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($softErrors);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(23) "https://example.com/#ab"
+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(2) "ab"
+}
+array(1) {
+  [0]=>
+  object(Uri\WhatWg\UrlValidationError)#%d (%d) {
+    ["context"]=>
+    string(2) "	b"
+    ["type"]=>
+    enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit)
+    ["failure"]=>
+    bool(false)
+  }
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/build_success_soft_errors_reset.phpt b/ext/uri/tests/whatwg/builder/build_success_soft_errors_reset.phpt
new file mode 100644
index 00000000000..4bfab1fd57e
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/build_success_soft_errors_reset.phpt
@@ -0,0 +1,44 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::build() - success - clears soft errors from a previous build
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("a\tb");
+$softErrors = [];
+$builder->build(softErrors: $softErrors);
+
+$builder->setFragment("ab");
+$url = $builder->build(softErrors: $softErrors);
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($softErrors);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(23) "https://example.com/#ab"
+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(2) "ab"
+}
+array(0) {
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt b/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
index f4940291f75..8cbbee1f5df 100644
--- a/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
+++ b/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
@@ -7,12 +7,12 @@
 $builder->setScheme("foo");
 $builder->setHost("example.com");
 $builder->setFragment("\tfo\no");
-$errors = [];
-$url = $builder->build(errors: $errors);
+$softErrors = [];
+$url = $builder->build(softErrors: $softErrors);

 var_dump($url->toAsciiString());
 var_dump($url);
-var_dump($errors);
+var_dump($softErrors);
 var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));

 ?>
diff --git a/ext/uri/tests/whatwg/builder/username_success_special_char.phpt b/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
index 5e18a2c66cc..76122e8be00 100644
--- a/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
+++ b/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
@@ -7,8 +7,8 @@
 $builder->setScheme("https");
 $builder->setHost("example.com");
 $builder->setUsername("~%#");
-$errors = [];
-$url = $builder->build(null, $errors);
+$softErrors = [];
+$url = $builder->build(null, $softErrors);

 var_dump($url->toAsciiString());
 var_dump($url);
diff --git a/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt b/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
index 956bd1653ad..9e920033ab7 100644
--- a/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
+++ b/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
@@ -7,12 +7,12 @@
 $builder->setScheme("\tfo\no");
 $builder->setHost("example.com");
 $builder->setUsername("f\no\ro\t");
-$errors = [];
-$url = $builder->build(errors: $errors);
+$softErrors = [];
+$url = $builder->build(softErrors: $softErrors);

 var_dump($url->toAsciiString());
 var_dump($url);
-var_dump($errors);
+var_dump($softErrors);
 var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));

 ?>
diff --git a/ext/uri/tests/whatwg/parsing/basic_success_reset.phpt b/ext/uri/tests/whatwg/parsing/basic_success_reset.phpt
new file mode 100644
index 00000000000..826442fac19
--- /dev/null
+++ b/ext/uri/tests/whatwg/parsing/basic_success_reset.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\WhatWg\Url::parse() - success - clears errors when there are no validation errors
+--FILE--
+<?php
+
+$errors = ["previous error"];
+$url = Uri\WhatWg\Url::parse("https://example.org/", errors: $errors);
+
+var_dump($url);
+var_dump($url->toAsciiString());
+var_dump($errors);
+
+?>
+--EXPECTF--
+object(Uri\WhatWg\Url)#%d (%d) {
+  ["scheme"]=>
+  string(5) "https"
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(11) "example.org"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(1) "/"
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+string(20) "https://example.org/"
+array(0) {
+}
diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c
index c07e8fae900..f48f9205352 100644
--- a/ext/uri/uri_parser_whatwg.c
+++ b/ext/uri/uri_parser_whatwg.c
@@ -987,7 +987,7 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors(zval *erro
 ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_from_zval(
 	lxb_url_t *lexbor_base_url, const zval *scheme, const zval *username, const zval *password,
 	const zval *host, const zval *port, const zval *path, const zval *query, const zval *fragment,
-	zval *errors_zv
+	zval *soft_errors_zv
 ) {
 	if (Z_TYPE_P(host) == IS_NULL ||
 		Z_STRLEN_P(host) == 0 ||
@@ -1028,7 +1028,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh
 	}

 	zval errors;
-	ZVAL_UNDEF(&errors);
+	array_init(&errors);

 	zend_result result = php_uri_parser_whatwg_scheme_write(lexbor_url, scheme, NULL);
 	php_uri_parser_whatwg_build_errors(&errors);
@@ -1085,13 +1085,32 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh
 		/* TODO */
 	}

-	if (php_uri_pass_errors_by_ref_and_free(errors_zv, &errors) == FAILURE) {
-		goto failure;
+	if (php_uri_pass_errors_by_ref_and_free(soft_errors_zv, &errors) == FAILURE) {
+		/* The errors zval was already consumed; goto failure would destroy it again. */
+		lxb_url_destroy(lexbor_url);
+		return NULL;
 	}

 	return lexbor_url;

 failure:
+	/* Include errors from earlier components in the exception raised by a later component. */
+	if (zend_hash_num_elements(Z_ARRVAL(errors)) > 0 && EG(exception)
+		&& instanceof_function(EG(exception)->ce, php_uri_ce_whatwg_invalid_url_exception)) {
+		zval rv;
+		zval *exception_errors = zend_read_property(php_uri_ce_whatwg_invalid_url_exception,
+			EG(exception), ZEND_STRL("errors"), true, &rv);
+		ZEND_ASSERT(Z_TYPE_P(exception_errors) == IS_ARRAY);
+
+		zval *error;
+		ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(exception_errors), error) {
+			Z_TRY_ADDREF_P(error);
+			zend_hash_next_index_insert(Z_ARRVAL(errors), error);
+		} ZEND_HASH_FOREACH_END();
+
+		zval_ptr_dtor(exception_errors);
+		ZVAL_COPY(exception_errors, &errors);
+	}
 	zval_ptr_dtor(&errors);
 	lxb_url_destroy(lexbor_url);
 	return NULL;
diff --git a/ext/uri/uri_parser_whatwg.h b/ext/uri/uri_parser_whatwg.h
index ed19a1067a5..360e8a2cd56 100644
--- a/ext/uri/uri_parser_whatwg.h
+++ b/ext/uri/uri_parser_whatwg.h
@@ -33,7 +33,7 @@ ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_port_validate(zend_long
 ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_from_zval(
 	lxb_url_t *lexbor_base_url, const zval *scheme, const zval *username, const zval *password,
 	const zval *host, const zval *port, const zval *path, const zval *query, const zval *fragment,
-	zval *errors_zv
+	zval *soft_errors_zv
 );

 ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_userinfo_percent_encode(const char *str, size_t str_length);