Repository navigation
Conversation
WSDL and XML schema errors were raised with soap_errorN(E_ERROR, ...). SoapClient turned the fatal error into a SoapFault and caught the bailout in SOAP_CLIENT_END_CODE(), but zend_bailout() had already called gc_protect(1), so the cycle collector stayed off for the rest of the request (phpGH-10026). The WSDL and schema loaders now throw the SoapFault with soap_throw_error() and return an error to their callers, which release what they own on the way out. The message and the fault code ("WSDL") are the same as before. SoapServer and SoapClient with the "exceptions" option disabled keep reporting these errors as fatal errors: the constructor turns the SoapFault back into the same E_ERROR once the loader has cleaned up. Since nothing bails out of the loaders any more, the error paths that existed only to survive a bailout are gone: the zend_try in load_wsdl_ex(), requestify_string() and the copies of targetNamespace in schema_load_file(). delete_type() now also frees an unresolved "ref", which an error before schema_pass2() can leave behind. Errors from encoding and decoding values still bail out and are not covered by this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Errors in encoding a request or decoding a response were raised with soap_errorN(E_ERROR, ...). SoapClient turned the fatal error into a SoapFault and caught the bailout in SOAP_CLIENT_END_CODE(), but zend_bailout() had already called gc_protect(1), so the cycle collector stayed off for the rest of the request (phpGH-10026). These errors are now reported with soap_encoding_error(). In a call of a SoapClient that uses exceptions it throws the same SoapFault as before (same message, fault code "Client", also stored in __soap_fault) and returns: the encoder returns a placeholder value that its callers can handle, and the call discards the request or the response. Only the first error is thrown; once it is pending, master_to_xml() and master_to_zval() skip the rest of the value, the request is not sent, and decoded values are no longer stored, so a typed property or the shared array of output headers cannot be written to. Skipping the rest also keeps a cycle of references that fails with "Nesting level too deep" from taking exponential time. SoapServer and SoapClient with the "exceptions" option disabled still get the same fatal error as before. get_conversion() can now return NULL after the error, so guess_xml_convert() and get_array_type() handle that instead of recursing or dereferencing it. One case behaves differently: if a callback (for example a typemap) has already thrown an exception when the error happens, that exception becomes the previous exception of the SoapFault. Before, the fatal error printed it as an uncaught exception warning and discarded it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
likemusic
force-pushed
the
soap-encoding-exceptions
branch
from
October 16, 2026 00:15
6e95138 to
a3d9633
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #24366, which does the same for WSDL and XML schema errors. This branch is on top of #24366, so the first commit here is #24366 itself; only the last commit is new ("ext/soap: report encoding errors without a bailout"). I will rebase once #24366 is merged or changed.
With both, #10026 is fixed for SoapClient: an encoding error in a call (for example
SOAP-ERROR: Encoding: object has no 'x' property, or an invalidxsd:intin the response) no longer leavesgc_status()['protected']attruefor the rest of the request.What changes
soap_errorN(E_ERROR, ...)calls inphp_encoding.cusesoap_encoding_error(). In a call of aSoapClientthat uses exceptions it throws the sameSoapFaultas before (same message, fault codeClient, also stored in__soap_fault) and returns. Everywhere else, i.e.SoapServerandSoapClientwithexceptions => false, it is the same fatal error as before.xmlNodePtr/zval *, so instead of propagating an error status through all of them, an encoder that hits an error returns a placeholder (aBOGUSnode orNULL) and the call discards the request or the response. ext/soap already works like this for exceptions thrown in the middle of encoding, e.g. theValueErrorfor recursive arrays into_xml_map()or for non-backed enums.master_to_xml()andmaster_to_zval()skip the rest of the value. Without that, a cycle ofhrefreferences that fails withNesting level too deepwould be decoded in exponential time, since nothing stops at the first failure any more.__getLastRequest()staysnull), decoded values are no longer stored into objects (a typed property of a classmap class would throw aTypeErrorthat hides theSoapFault), decoding of response headers stops (the trace of theSoapFaultholds the array of output headers, writing to it trips an assertion inzend_hash), and a<Fault>whose content fails to decode does not replace__soap_fault.<boolean>with an element inside, an invalid timestamp, unresolved references (they return the node itself, notNULL).get_conversion()can returnNULLnow, soguess_xml_convert()(which would otherwise recurse throughUNKNOWN_TYPEforever) andget_array_type()handle that.One case behaves differently, on purpose: if a callback (for example a typemap
to_xml) has already thrown an exception when the encoding error happens, that exception is now the previous exception of theSoapFault. Before, the fatal error printed it as an "Uncaught ..." warning and discarded it.The server keeps its fatal errors:
SoapServer::handle()answers with a SOAP fault and ends the request, so the GC state does not matter there, and changing it would be a much larger change ofhandle().Testing
Debug build of master (
de7ff46337f) with #24366, unless noted otherwise.ext/soap: 520 tests pass.bugs/gh10026_encoding.phpt(one encoding and one decoding error, then 20000 cycles are collected);encoding_errors_no_bailout.phpt, which goes through 45 encoding and decoding errors with aSoapClient, including the cases above (first error wins, output headers, typed classmap property, a fault that fails to decode); andencoding_errors_nesting.phptforNesting level too deepon an array that contains itself once and 8 times, skipped with ASAN like theGHSA-rgrp-mwpx-f6rmtests because of the stack depth. All three fail on master without this change, and only on the lines that check the GC state; the messages are the same.SoapClientwith exceptions,SoapClientwithexceptions => false, andSoapServer:__soap_fault,__getLastRequest(), output headers and exit code are the same in all 59 cases;protectedistrueafter the 50 errors on unpatched master andfalseafter all of them with the patch, and 1000 cycles created afterwards are collected (0 on unpatched master). The only other difference is thepreviousexception described above;exceptions => falseandSoapServer: the output is byte-for-byte the same before and after (50 fatal errors / SOAP faults);Error calling to_xml/from_xml callback(call_user_function()only fails when the executor is inactive), the twoInvalid value for type 'base64Binary'(php_base64_decode()is not strict and does not fail), the twoInternal Error(thedefaultof a switch over all type kinds), andFailed to encode list itemfor a list given as a string.USE_ZEND_ALLOC=0: the corpus in all three modes gives no report, with LeakSanitizer enabled for theSoapClientruns (leak detection off for the fatal-error modes).run-tests --asanonext/soap: the new tests pass; the same 6 tests as on master fail with LeakSanitizer reports (bug37013,bug55323,gh16318,gh22585,r3_groupE_list_005w,r3_groupE_list_006w).While testing I found an existing problem, independent of this PR: if a typemap
from_xmlcallback throws while a response header is decoded, the next header is written to the output headers array that the exception trace shares, and a debug build fails with_zend_hash_str_add_or_update_i: Assertion ... failed(same on master). The fix for PHP-8.4 is #24368.🤖 Generated with Claude Code