Skip to content

Breaking Change in Php8.6 Nightly DOMDocument->loadHTML on Win11 #24300

Closed as not planned

Description

@oleibman

Description

The following code:

<?php
        $dom = new \DOMDocument();
        $prefix = '<?xml encoding="UTF-8">';
        $html = 'foo';
        $dom->loadHTML($prefix . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        $out = $dom->saveHTML();
        echo $out;

Resulted in this output (Windows Php8.6 nightly):

<!--?xml encoding="UTF-8"-->

But I expected this output instead (all other environments):

<?xml encoding="UTF-8"><p>foo</p>

This is the output from all Php8.2-8.5 for both Windows and Unix, and I believe it is also the output from Php8.6 nightly for Unix. I do not have a Unix Php8.6 testbed, but this seems to be the result at Github using 8.6 nightly.

This represents a major break for our product.

PHP Version

PHP 8.6.0-dev (cli) (built: Oct 12 2026 23:45:52) (ZTS Visual C++ 2026 x64)
Copyright © The PHP Group and Contributors
Zend Engine v4.6.0-dev, Copyright © Zend by Perforce
    with Zend OPcache v8.6.0-dev, Copyright ©, by Zend by Perforce

Operating System

Windows 11

Activity

  1. ndossche commented on Oct 13, 2026

    @ndossche
    Member

    This is actually not caused by a code change in PHP, but by a dependency update of libxml. DOMDocument uses libxml to parse HTML documents, which isn't compliant to the HTML spec. Lately, the libxml maintainers have made it more spec compliant (but still not fully).
    So the old hack you were using breaks on newer libxml versions. This is actually already broken for Linux distros that ship a more modern libxml version. And the hack was also never actually safe to begin with.
    The only solution is to ditch DOMDocument and switch to the newer Dom\HTMLDocument class that was introduced in PHP 8.4 to deal with all these kinds of troubles.

  2. oleibman commented on Oct 13, 2026

    @oleibman
    Author

    Thank you for the explanation. Is there a specific change in libxml that you can point me to so that I can explain the behavior change to my users?

    I need to support 8.3 (and for a while 8.2), so HTMLDocument isn't a good choice for me. It may be possible, but if the present code is using an old hack, this will certainly be introducing a new one in its place.

    Or perhaps I'm missing something. I believe the present code needed the xml tag to make sure our (user-supplied) UTF-8 string wasn't parsed as ISO-8859-1. Is there a better way to do that with DOMDocument?

  3. ndossche commented on Oct 13, 2026

    @ndossche
    Member

    4. Thank you for the explanation. Is there a specific change in libxml that you can point me to so that I can explain the behavior change to my users?

    The changes landed in multiple commits, a full breakdown is provided here: https://gitlab.gnome.org/GNOME/libxml2/-/work_items/211

    I need to support 8.3 (and for a while 8.2), so HTMLDocument isn't a good choice for me. It may be possible, but if the present code is using an old hack, this will certainly be introducing a new one in its place.

    I see.

    Or perhaps I'm missing something. I believe the present code needed the xml tag to make sure our (user-supplied) UTF-8 string wasn't parsed as ISO-8859-1. Is there a better way to do that with DOMDocument?

    Probably, prepending '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">' is better than prepending the xml declaration.

  4. oleibman commented on Oct 13, 2026

    @oleibman
    Author

    This seems like a complete disaster to me.

    $dom->loadHTML('<table<>');

    That used to correctly give warning messages. Now it doesn't - it just throws everything out without any messages and without any indication of failure. How are we supposed to tell what's going on?

  5. ndossche commented on Oct 14, 2026

    @ndossche
    Member

    It depends on what you expect to happen.
    The behaviour that libxml had previously was specific to whatever version your client was using and was closer to parsing html version 4 than modern html.
    The newer libxml parser is closer to html5 and its warning/error model.
    There's nothing we can do at the php side.

    The only real solution is to use the new dom classes on php 8.4 and higher: https://3v4l.org/#v853
    For lower php versions, clients should stick to older libxml versions, but note those libxml versions come with unbackported security issues.

  6. DanielRuf commented on Oct 15, 2026

    @DanielRuf
    Contributor

    So I guess the ne DOM class is not directly supported on older PHP versions and there are no polyfills.

    I did some research and came to this solution:

    const UNICODE_START_NON_ASCII = 0x80;
    const UNICODE_MAX_CODE_POINT  = 0x10ffff;
    const MB_MAP_OFFSET           = 0;
    const MB_MAP_BITMASK          = 0x1ffff; // 0x1fffff may be alternatively used, which accepts characters from planes above U+20000 including CJK, emojis and more
    
    $doc = new \DOMDocument();
    
    $convmap = [
    	UNICODE_START_NON_ASCII,
    	UNICODE_MAX_CODE_POINT,
    	MB_MAP_OFFSET,
    	MB_MAP_BITMASK
    ];
    $encodedHtml = mb_encode_numericentity('<body>' . $valid . '</body>', $convmap, 'UTF-8');
    
    $doc->loadHTML(
    	$encodedHtml,
    	LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
    );

    Not beautiful but it works for me.

  7. ndossche commented on Oct 15, 2026

    @ndossche
    Member

    So I guess the ne DOM class is not directly supported on older PHP versions and there are no polyfills.

    This is correct, it is too complicated and too much functionality to reasonably polyfill.

    Your solution should indeed work.

  8. oleibman commented on Oct 15, 2026

    @oleibman
    Author

    Thank you @DanielRuf. I think pre-pending the string with a UTF-8 BOM may also work. At any rate, I am well on my way to a solution. It's just hard to figure out what else may now be different aside from what I've already found.

  9. oleibman commented on Oct 16, 2026

    @oleibman
    Author

    Here's another difference I found. Just adding this for documentary purposes. This is a genuine bug-fix for libxml; our code just happens to depend on the bug (sigh). When we pass the string <p>foo</p><p>bar</p> to loadHTML() and then iterate through all the child nodes, what the old libxml generates is:

    nodeName = p, nodeValue = foobar // this is clearly wrong
    nodeName = #text, nodeValue = foo
    nodeName = p, nodeValue = bar
    nodeName = #text, nodeValue = bar
    

    As further evidence, when we do saveHTML() in this case, what we see is the obviously wrong <p>foo<p>bar</p></p>.

    Pre-pending with UTF-8 BOM or using @DanielRuf suggestion does not affect this. However, prepending the xml tag results in:

    nodeName = xml, nodeValue = encoding="UTF-8"
    nodeName = p, nodeValue = foo // this is now correct
    nodeName = #text, nodeValue = foo
    nodeName = p, nodeValue = bar
    nodeName = #text, nodeValue = bar
    

    As I said, this particular change in libxml is a bug-fix after all. We can continue to prepend the xml tag when we know we're using a libxml version below a certain threshold.

  10. DanielRuf commented on Oct 16, 2026

    @DanielRuf
    Contributor

    We can continue to prepend the xml tag when we know we're using a libxml version below a certain threshold.

    I thought also using a libxml version check (either using LIBXML_VERSION or LIBXML_LOADED_VERSION) but this is something that some want to avoid like PHP_VERSION_ID checks.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions