Problem/Motivation

When testing various migrations. It is too easy to jam up when the file attachments are not present at the URLs in the XML file. (whether 404, server is missing, etc)

This occurs when the import imagefield is selected but images cannot be loaded by url.

in consideration of #2742279: Rewrite local link/image references in content, switch image content domains we should be able to replace these remote URL domains with a new field in the initial config for optional replacement domain (eg. wordpress staging domains reachable from our box)

Steps to reproduce

Try to import anything with file attachment / featured image with unavailable URL

Proposed resolution

skip on error for missing file attachment / image URLs.

include a replacement domain URL field in the config.

i think this should be configurable ideally, have timeout and so on.

Remaining tasks

Not certain. Maybe, if the image field is not selected, do not create the attachments migration at all, there is no if statement here. Unlike the tag_vocabulary right below it. If so, then it would need to not be a dependency for content post and content page.

    // Set up the attachment migration.
    $this->attachmentID = $this->configuration['prefix'] . 'wordpress_attachments';
    $migration = $this->createEntityFromPlugin('wordpress_attachments', $this->attachmentID);
    $migration->set('migration_group', $this->configuration['group_id']);
    $process = $migration->get('process');
    $process['uid'] = $this->uidMapping;
    $migration->set('process', $process);
    $migration->save();

User interface changes

Improve the UI message. perhaps as a placeholder until this is improved.

API changes

Data model changes

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

hongpong created an issue. See original summary.

hongpong’s picture

Issue summary: View changes
ressa’s picture

Yes, I ran into this during migration testing with FakerPress generated content, which served its purpose, generating different variations of non-importable image data. It would be nice if dirty data does not cause the migration to a full stop, but alerts the user that there's a problem, whatever is the easiest and simplest method.

3 out of 11 items failed due to malformed data, and after deleting those three items, the import completed. Posts with this kind of image data failed, stopping the migration hard (copied out from each <item>):

  •   <wp:postmeta>
        <wp:meta_key>_thumbnail_id</wp:meta_key>
        <wp:meta_value><![CDATA[a:0:{}]]></wp:meta_value>
      </wp:postmeta>
    
  •   <wp:postmeta>
        <wp:meta_key>_thumbnail_id</wp:meta_key>
        <wp:meta_value><![CDATA[raw]]></wp:meta_value>
      </wp:postmeta>
    
  • <wp:postmeta>
        <wp:meta_key>_thumbnail_id</wp:meta_key>
        <wp:meta_value><![CDATA[a:1:{s:17:"svg-not-supported";a:1:{i:0;s:78:"SVG images are not supported. Please use a different image provider or format.";}}]]></wp:meta_value>
      </wp:postmeta>
    

The error reports in the migration:

my_wordpress_content_post:field_image:migration_lookup: Drupal\Core\Database\DatabaseExceptionWrapper was thrown while attempting to stub: SQLSTATE[22007]: Invalid datetime format: 1366 Incorrect integer value: 'a:0:{}' for column `db`.`migrate_map_my_wordpress_attachments`.`sourceid1` at row 1: INSERT INTO "migrate_map_my_wordpress_attachments" ("source_ids_hash", "sourceid1", "source_row_status", "rollback_action", "hash", "destid1", "last_imported") VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6); Array ( [:db_insert_placeholder_0] => 898ad8566a298470c93d76cd23eaeab85040484209914373e29e6a080157fdf1 [:db_insert_placeholder_1] => a:0:{} [:db_insert_placeholder_2] => 1 [:db_insert_placeholder_3] => 0 [:db_insert_placeholder_4] => [:db_insert_placeholder_5] => 23 [:db_insert_placeholder_6] => 1773570713 )

my_wordpress_content_post:field_image:migration_lookup: Drupal\Core\Database\DatabaseExceptionWrapper was thrown while attempting to stub: SQLSTATE[22007]: Invalid datetime format: 1366 Incorrect integer value: 'raw' for column `db`.`migrate_map_my_wordpress_attachments`.`sourceid1` at row 1: INSERT INTO "migrate_map_my_wordpress_attachments" ("source_ids_hash", "sourceid1", "source_row_status", "rollback_action", "hash", "destid1", "last_imported") VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6); Array ( [:db_insert_placeholder_0] => bd2128fbcdb3cc3d8bc42b7fe69b31aed3e0bc7b2ae9c5bd40e46d014848ce76 [:db_insert_placeholder_1] => raw [:db_insert_placeholder_2] => 1 [:db_insert_placeholder_3] => 0 [:db_insert_placeholder_4] => [:db_insert_placeholder_5] => 25 [:db_insert_placeholder_6] => 1773570713 )

my_wordpress_content_post:field_image:migration_lookup: Drupal\Core\Database\DatabaseExceptionWrapper was thrown while attempting to stub: SQLSTATE[22007]: Invalid datetime format: 1366 Incorrect integer value: 'a:1:{s:17:"svg-not-supported";a:1:{i:0;s:78:"SVG images are not supported. Please use a different image provider or format.";}}' for column `db`.`migrate_map_my_wordpress_attachments`.`sourceid1` at row 1: INSERT INTO "migrate_map_my_wordpress_attachments" ("source_ids_hash", "sourceid1", "source_row_status", "rollback_action", "hash", "destid1", "last_imported") VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6); Array ( [:db_insert_placeholder_0] => fee26a084c6ef67b4b0e4100a7d313a6ccbc12ef2eddd42c476edb25818ca411 [:db_insert_placeholder_1] => a:1:{s:17:"svg-not-supported";a:1:{i:0;s:78:"SVG images are not supported. Please use a different image provider or format.";}} [:db_insert_placeholder_2] => 1 [:db_insert_placeholder_3] => 0 [:db_insert_placeholder_4] => [:db_insert_placeholder_5] => 26 [:db_insert_placeholder_6] => 1773570713 ) 

hongpong’s picture

Status: Active » Needs review

I have a possible code for processing these fetches better. Can you run with roughly the same setup as before? Will try this MR47 .. https://git.drupalcode.org/project/wordpress_migrate/-/merge_requests/47 Got this spun up w Claude sonnet.

hongpong’s picture

okay this will also help for logging a great deal, it now tells missing hostnames etc in watchdog, may also help #2742279: Rewrite local link/image references in content, switch image content domains and fatal date error #3516516: DatabaseExceptionWrapper: Invalid datetime format

https://git.drupalcode.org/project/wordpress_migrate/-/merge_requests/47... i would say give that a try and see what you get including in the logs (watchdog etc). the config for the module has been updated, in the new /docs/ file i have the "drush cim" command to add the new config settings that control logging level.

i have not tried it in drush but its good in the wizard UI. I need to head out for a while but I hope this helps move things.

ressa’s picture

Status: Needs review » Needs work

Thank you so much, this is a very nice improvement, and I get great feedback now with Drush.

I can warmly recommend creating a lot of test content with FakerPress (with more or less dirty data) for testing purposes and combine it with the amazing DDEV snapshot, to allow very speedily restoring a fresh instance within seconds, and then run the whole shebang again with Drush :)

If I run a migration with dirty data, I get actionable feedback, like below, and I can fix these three instances:

  • <wp:meta_value><![CDATA[a:1:{s:17:"svg-not-supported" ...

    $ drush migrate:import my_wordpress_content_post
      1/31 [>---------------------------]   3% [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
      4/31 [===>------------------------]  12% [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id raw ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
      7/31 [======>---------------------]  22% [error]  Attachment post_id a:1:{s:17:"svg-not-supported";a:1:{i:0;s:78:"SVG images are not supported. Please use a different image provider or format.";}} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id raw ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id a:1:{s:17:"svg-not-supported";a:1:{i:0;s:78:"SVG images are not supported. Please use a different image provider or format.";}} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     13/31 [===========>----------------]  41% [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [notice] Processed 22 items (22 created, 0 updated, 0 failed, 0 ignored) - done with 'my_wordpress_content_post'
  • <wp:meta_value><![CDATA[raw]]></wp:meta_value>

    $ drush migrate:import my_wordpress_content_post
      1/29 [>---------------------------]   3% [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
      3/29 [==>-------------------------]  10% [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id raw ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
      6/29 [=====>----------------------]  20% [error]  Attachment post_id raw ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     12/29 [===========>----------------]  41% [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [notice] Processed 22 items (22 created, 0 updated, 0 failed, 0 ignored) - done with 'my_wordpress_content_post'
  • <wp:meta_value><![CDATA[a:0:{}]]></wp:meta_value>

    $ drush migrate:import my_wordpress_content_post
      3/28 [===>------------------------]  10% [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id raw ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
      6/28 [======>---------------------]  21% [error]  Attachment post_id raw ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     14/28 [==============>-------------]  50% [error]  Attachment post_id a:0:{} ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
     [notice] Processed 22 items (22 created, 0 updated, 0 failed, 0 ignored) - done with 'my_wordpress_content_post'

The migration still stops hard though, if there's a problem ... would it be possible to simply log these problems, but let the migration continue? Or maybe that's in the pipeline for the solution here?

...

Also, about the improved feedback: When I run the import I see these notices, about inline images I think. For some reason, I need to run drush migrate:import --all twice to get attachments import to complete:

$ drush migrate:import --all
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_content'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_tags'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_categories'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_attachments'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_comment'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_authors'
 [warning] Attachment post_id 115 ("Picsum ID: 760"): replacement_domain "constants/replacement_domain" could not be parsed. Using original URL "https://wp.ddev.site/wp-content/uploads/2026/03/b6184e23-3148-36a0-b0a2-5b8e9bd04bb4.jpg".
  1/54 [>---------------------------]   1% [warning] Attachment post_id 117 ("Picsum ID: 940"): replacement_domain "constants/replacement_domain" could not be parsed. Using original URL "https://wp.ddev.site/wp-content/uploads/2026/03/68088ca8-d4c2-39c6-b4bf-4bb98b69599f.jpg".
 [warning] Attachment post_id 119 ("Picsum ID: 501"): replacement_domain "constants/replacement_domain" could not be parsed. Using original URL "https://wp.ddev.site/wp-content/uploads/2026/03/17d71476-ce75-3442-a489-63f8f0dbe58d.jpg".
 [warning] Attachment post_id 121 ("Picsum ID: 170"): replacement_domain "constants/replacement_domain" could not be parsed. Using original URL "https://wp.ddev.site/wp-content/uploads/2026/03/f3eba549-9754-3f41-8c94-370a7aba5aca.jpg".

Also, it seems like a new hard blocker has been introduced. I am not sure what the cause is, since the source data has previously completed a migration:

$ drush migrate:import my_wordpress_content_post
  3/24 [===>------------------------]  12% [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 
  8/24 [=========>------------------]  33% [error]  Attachment post_id 0 ("(no title)"): attachment_url is empty or not a string (NULL). Skipping. 

The attachment post challenge above blocks progress from here on, to finalise the import of the comments, and completing the migration:

$ drush migrate:import --all
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_content'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_tags'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_categories'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_attachments'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_comment'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'wordpress_authors'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'my_wordpress_attachments'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'my_wordpress_authors'
 [notice] Processed 0 items (0 created, 0 updated, 0 failed, 0 ignored) - done with 'my_wordpress_categories'
 [error]  Migration my_wordpress_comment_post did not meet the requirements. Missing migrations my_wordpress_content_post. 

In MigrateToolsCommands.php line 1184: my_wordpress_comment_post migration failed.
Failed to run drush migrate:import --all: exit status 1

Status:

$ drush migrate:status
 ------------------------------------- --------------------------- -------- ------- ---------- ------------
  Group                                 Migration ID                Status   Total   Imported   Unprocessed
 ------------------------------------- --------------------------- -------- ------- ---------- ------------
  Imports from WordPress site (my_wp)   my_wordpress_attachments    Idle     54      54         0          
  Imports from WordPress site (my_wp)   my_wordpress_authors        Idle     19      18         0          
  Imports from WordPress site (my_wp)   my_wordpress_categories     Idle     91      91         0          
  Imports from WordPress site (my_wp)   my_wordpress_comment_post   Idle     30      0          30         
  Imports from WordPress site (my_wp)   my_wordpress_content_page   Idle     2       0          2          
  Imports from WordPress site (my_wp)   my_wordpress_content_post   Idle     24      22         2          
  Imports from WordPress site (my_wp)   my_wordpress_tags           Idle     0       0          0          
  Imports from WordPress site (my_wp)   my_wordpress_comment_page   Idle     30      0          30         
 ------------------------------------- --------------------------- -------- ------- ---------- ------------
hongpong’s picture

would it be possible to simply log these problems, but let the migration continue?

In theory yes that should be what it does, it should just skip. I think it is possible that 'error' level alerts will stop things cold, that is how the configuration validator works but I haven't tried with the attachments yet. I just tested this MR to make sure that the logging was coming thru. thanks for taking it for a spin and updating the documentation carefully.

I feel we may want to commit this MR 47 if it seems like it doesn't make things worse - but if it is halting just because an image can't be retrieved we do not want that to be happening. They should just be "Warning" not "error" in PSR terms.

in addition re the domains can you try looking at replacement_domain: '' in wordpress_attachments.yml? i think its not implemented in the wordpress migrate UI GUI but it could be relevant here.. (this was auto generated with claude and i have not tested yet). replacement_domain "constants/replacement_domain" could not be parsed. this seems like something was incorrect.

oh looking at this more carefully. i think these should not be in quotation marks. in the wordpress_attachments.yml.

    replacement_domain: 'constants/replacement_domain'
    timeout: 'constants/timeout'

I will also mention that this test XML - the attachments are all valid there - https://gitlab.com/HongPong/wordpress-test-imports/-/blob/v1.0.0/wp-impo...

I just tried reworking this stuff but no dice (re the constants/replacement domain issue, it clearly needs to be redone. the variable is not passing thru correctly). I also tried adding debug reporting and that didn't work either . byahhh

ressa’s picture

That sounds fantastic, I'll be ready to try it out, after you have had time to look at it, and if something is ready for testing at some point.

And once we get it working, and it handles dirty data more gracefully, and logs and reports missing images while continuing the import, it's probably worth considering adding such dirty data -- like a few variants of faulty image attachments like <wp:meta_value><![CDATA[a:0:{}]]></wp:meta_value> -- to the test XML, to make sure they are handled well.

About committing this #47 MR now or wait, I'll leave that up to you to decide. But if a halting migration due to dirty data is not acceptable, then it probably needs some more tweaking.

PS. About LLM's, have you seen #3574093: Ban slop issue summaries and comments? :)

hongpong’s picture

I think I should hold on the MR or perhaps subdivide it. I think it should not halt, but instead it should clearly notify the user what has happened.

Thank you for linking the issue, I had not seen it! Dries has a new one - https://dri.es/never-submit-code-you-do-not-understand - which I should try to stick by. Our situation with this module includes so many dirty inputs possible, more than a usual challenge. I agree adding the dirty ones is helpful for tests. I am very sympathetic to the argument that these vibe coding giant PRs are harmful to code reviewers. Ironically when I've been to the DrupalCamp NJ last couple years those were the most forward on AI usage especially on content review.

Zivtech warned that LLM are bad at using existing frameworks to solve problems, they tend to re implement existing systems (as noted in the thread as well). I have noticed that the LLM will tend to regenerate checker / helper functions duplicatively and in poorly organized ways. Zivtech created a system called drupal critic which is intended to put these things in check somewhat https://lobehub.com/skills/zivtech-drupal-critic-drupal-critic - https://www.zivtech.com/blog/ai-driven-code-reviews-transforming-efficiency - some of the niftiest stuff I have yet heard of. More here - https://www.zivtech.com/blog/claude-skills-and-mcp-drupal-development-dr... - slides https://www.drupalcampnj.org/sites/default/files/2026-03/Skills-MCPs-Dru...

I have also noticed that the logger I added (which I intended to apply in all places of the module) does not have the same signature as the logger in the MR. The already committed logger was mainly LLM generated and I think it is already improving our situation - I would like to just keep adapting it. However now I have not implemented it correctly using the LLM in this MR, and that should be a blocker on the MR I now think.

ressa’s picture

Yes, Dries's article was shared on Planet Drupal, so I already read it. Creating Drupal code with LLM is an extremely interesting subject, and divisive as can be seen in the issue with the hot debate taking place right now :)

I am impressed by the strong positions against LLM code for Drupal, from who I consider the elite of Drupal coders and core contributors. @phenaproxima mentioned banning users who don't disclose LLM-created MR's in #3568936: Embrace the chaos, add a couple of AGENTS.md file to core, and I floated the idea of banning LLM code contributions in Drupal, or perhaps allow it, but with VERY strong guard rails.

Several coders, like _nod almost got addicted to using LLM's, and this guy even feels he lost the ability to code: I was a 10x engineer. Now I'm useless.

Thanks for the Zivtech links! Interesting tools and articles by Zivtech, but like you noticed, LLM-generated code is not systematic at all, and how can it be? LLM's are not system architects, we are! An LLM is in essence just a glorified "guess the next word"-machine ... So it may be a useful tool for very small, confined tasks, but surely not for large chunks of code, where all kinds of hallucinations can sneak in. Interesting times!