Problem/Motivation
The LinkItem field schema specifies the options property as a serialized blob that is not required. This results in a database column with the longblob type whose value may be NULL.
In a normal editing workflow there doesn't seem to be a problem. The field always assigns values to the options column. But there are other circumstances where data may be inserted into Link fields and the options are allowed to be NULL. Examples of this include translated Paragraphs and migrated data. When this happens an error occurs:
TypeError: Unsupported operand types: null + array in Drupal\link\Plugin\Field\FieldFormatter\LinkFormatter->buildUrl() (line 249 of core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php).
Steps to reproduce
The easiest way to reproduce the error is to edit a Link field in the database.
- Add a Link field to any node type. You can use the default options.
- Create a new node of that type. Fill out the Link field.
- View the node you just created. Note that everything is working correctly.
- Update the database record for the node you just created in the Link field's table. Set the options column to NULL. Note that this is an accepted value that doesn't cause a database error.
- (optional) clear the cache, just in case.
- View the node again and get a WSoD.
Proposed resolution
Handle the NULL case in LinkFormatter.
Remaining tasks
- Make any necessary updates to the MR, including test improvements.
- Review.
- Commit.
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|
Issue fork drupal-2871217
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
Comment #2
michelleAnd the patch...
Comment #3
dawehnerI'm wondering whether we can prevent
$item->optionbeingNULLin the first place?Comment #4
michelleThat would be ideal but I don't know how it's happening. My best guess is it's a Paragraphs translation error since it's happening in a link field on a paragraph on various translated nodes. Trying to figure out exactly where it's going wrong could be a lot of digging. I understand if this doesn't make it into core but we needed to patch it to get it working for the client so might as well make an issue, just in case. :)
Also, editing the node, clicking "edit" on the paragraph so that the link field is visible, and then re-saving the node fixes the problem. That's another reason I think something is going wrong when the translation is being created.
Comment #5
mac_weber commentedDoes it happen for all 3 Link field configs? Internal, External, and Internal or External?
This issue may be related (or maybe a duplicate) to #2802403: Combination of language negotiation and path aliasing can cause a corrupted route cache, 404s
Comment #6
dawehnerI think no question, we should fix this, but understanding where this is coming from might result into an underlying bug we should fix as well / instead. Hiding notices, can have small negative impacts.
Comment #7
michelleI will try digging into it more and see if I can reproduce it. It was on a client site and resaving the paragraph would fix it so I went through all my known examples in the process of finding the work-around. I will see if I can re-create it by translating something on their site. If I can do that, will see if I can get it to reproduce on a more vanilla environment.
Comment #9
fgmWe have the same issue, also in a multi-level paragraphs context, on a monolingual french site.
The patch could be simplified, I think:
Could be written more simply as just:
Comment #10
fgmRerolled. Testing against 8.4.x since this is a just a bug which could still go into 8.4.x.
Comment #11
fgmRerolled on today's 8.5.x HEAD.
Comment #12
dawehnerWe should expand the test coverage in
\Drupal\Tests\link\Functional\LinkFieldTest::testLinkFormatter.Comment #14
FAAREIA commentedThank you all for the patch.
I'm using Drupal 8.4.5 and the patch #11 worked.
I was having this issue with certain nodes AND on certain pages number from blocks generated with views.
For example:
Comment #15
longwaveI tried modifying testLinkFormatter to run
but it is expected to be an array elsewhere!
I do also wonder if fixing this in LinkFormatter is correct or if we should enforce it higher up the chain, e.g. PluginSettingsBase:;getSettings() claims to return an array according to the interface docs, so why shouldn't it always do that?
Comment #16
ndobromirov commentedHi,
I've tracked this to a field on a node type. Not related to paragraphs or translations.
Nodes are imported through custom code, so fields are initialized with the following statements:
This works correctly* (no exceptions are thrown) to write the URI but as options and title are not required (on API level) and apparently no defaults get stored in DB as well, once it gets read from there it's plain wrong, as sensible defaults are not applied at that point on field level.
Here is an extract from my field's table. URLs are sanitized...
The items are already incomplete after this call.
A correct fix would be:
Option 1: (I like this one better)
To fix the save behavior on the field, so defaults are applied correctly before data is saved.
Have an update hook to resolve existing data mutations in DB, so invalid data is fixed.
Option 2:
Have the defaults applied correctly on field read, so all dependent code (widgets, formatters, etc.) will HAVE valid values.
Comment #17
ndobromirov commentedThis is based on the patch from #11.
Here is a patch that adds the requested test tweaks in #12.
Comment #19
ndobromirov commentedAnother attempt at a fix.
Comment #20
ndobromirov commentedChanging status...
Comment #21
maticb commentedI did not test the patch, but I would just like to add for anyone that might have similar issues as me:
When importing a D7 field that included the twitter username, which I had to migrate to a link field type, I passed an empty string to "options", ended up with this error, and fixing it like so (inside a process plugin):
Just wanted to drop a note, even though fixing this automatically on migrations would probably call for a separate issue on the migrate module?
Comment #23
unstatu commentedIt works great. +1
Comment #24
djg_tram commentedMay I urge you to add that
(array)finally? The same error crops up with all updates again and again.Comment #26
kristen polWoohoo! Patch #19 works great for me. Thanks! :)
Comment #27
nickbumgarner commentedPatch #19 worked for me as well. Thanks
Comment #28
ndobromirov commentedAs per #23, #24, #26 and #27 moving to RTBC.
Comment #29
catchThis looks like corrupted data to me, could we look again at 'option 1' from #16?
Comment #30
fgmIt would be ideal if we could find the actual source (option 1 #16), but it won't fix the issues for all the sites having been corrupted in the meantime. So even though the complete fix should be at the error root, I think it should still include at least the one-word fault-resilience patch. (the (array) cast) which has about zero cost and make core more resilient.
Comment #31
djg_tram commentedAdd that (array) from patch #19 right now! Today! This is insane. I spent nearly an hour to find out what went wrong with a Drupal update on a site. And when I finally found this thread, I was shocked to see that I had the same problem about a year ago, and had to come here to urge you to add that single cast, absolutely free, no performance penalties, nothing. It was the same darn error back then, and of course, long forgotten, and it appears again and again, about a year later. I don't care about the reasoning that it shouldn't come up, yes, sure, there is some strange data somewhere but not all sites are re-made every six months from scratch, some are ported from version to version, have lots of stored data, and so on. It's really unacceptable to leave out such a simple solution that sanitizes even a situation that you don't think will crop up but nonetheless, it does.
Add that cast now.
Comment #33
djg_tram commented#31. Another Drupal release comes, 8.8, and again, and again, and again, upgrade kills the site to a WSOD because you refuse to add that single obvious cast. Why?
Comment #34
ndobromirov commented@djg_tram
You can use composer patches and have the patch applied automatically on updates until this is resolved.
This should fix the WSODs caused by this issue, as well as you will know sooner if the existing patch does not apply on the latest version.
In the meantime, I am a HUGE +1 on the two comments from #30 and #31.
Comment #35
wizonesolutionsIs anyone sitting on an 8.8.x-compatible patch for this? Can you upload it? Otherwise, I will do so in a bit (based on #19, which we were using before).
Comment #36
wizonesolutionsThis is for 8.8, but figured I'd run 8.9 tests as well in case anyone needs that. This shouldn't be RTBC'd even if the tests pass since it's not actually against dev.
Comment #37
wizonesolutionsBack to Needs work so someone can actually roll a patch against 8.9.x-dev
Comment #38
sivaji_ganesh_jojodae commentedAttached is the patch re-rolled against 8.9-dev.
Comment #39
dwwFor PHP7+ this can just be:
$settings = $configuration['settings'] ?? [](null coalesce operator)
Otherwise, patch seems reasonable. Didn't super-closely review nor test manually, so not RTBCing.
Comment #40
sivaji_ganesh_jojodae commentedSure. Updated the patch to use the null coalesce operator.
Comment #41
ghost of drupal pastLooks good to me. Removing the special casing in the formatter indicates this is on the right path.
Comment #42
alexpottIf i remove this change and run the test it passes - so this bit of the change doesn't have test coverage. However if I remove the other part of the fix and leave this in the test fails - so is this bit actually necessary? And if so can we prove it by adding a test. I think it is because it's the bit that sorts out the NULL options whereas the other stuff is for the NULL settings.
Also now that we have PHP7 rather than an array cast here we could do
$item->options ?? [];because that's more specific to the NULL case.Comment #43
ravi.shankar commentedHere this patch might fix comment #42
Comment #44
alexpott@ravi.shankar that addresses the last part but not the
part
Comment #46
kristen polBack to needs work to address #44.
Comment #55
larowlanCrediting folks from #3174350: Unsupported operand types in LinkFormatter->buildUrl() which I marked as a duplicate
Comment #56
larowlanAccording to reports in #3174350: Unsupported operand types in LinkFormatter->buildUrl() this causes a WSOD on php7.4, so raising this to major
Comment #57
codesmithUsing Drupal 8.9 and php 7.3. I was getting a WSOD when trying to edit a translated version of a node that uses paragraphs. Applied patch in #43 and seems to be working fine. Thanks!
Comment #58
yogeshmpawarComment #60
droath commentedI ran into a weird issue where the options was set to
s:6:"a:0:{}";, which then returned a string. I was able to make a minor change to the patch #43 which adds a check to make sure it's an array.Comment #61
jsutta commented#60 worked for me in Drupal 9.3.12 with PHP 8.0.
Comment #62
kristen pol@jsutta Would you please explain how you tested? Thanks.
Comment #64
volker23 commentedPatch #60 worked for my use-case. We had a migration from D7 to 9.3.14. There was a link field to migrate and the migration snippet looks like this:
All of the migrated nodes showed this when we're trying to view them (although editing was possible) :
After applying the patch, everything worked as expected.
Comment #65
papagrandeThis bug caused a WSOD on a production server (Drupal v9.4.5) when I added a link field to a paragraph (v1.15.0) that then got translated across multiple languages via Lingotek. A workaround was to add the language prefix to each translated URL, but patch #60 fixed it for me.
Edit: I think Lingotek may be corrupting the link when translating because when I enter the English URL on a French paragraph it works fine without the patch.
Comment #66
ravi.shankar commentedAdded reroll of patch #60 on Drupal 9.5.x, and added reroll diff.
Also added interdiff between patch #43 and #patch 60.
I think it's not ready for RTBC as per comment #44, so back to needs work.
Comment #67
ravi.shankar commentedFixed Drupal CS issue of patch #66.
Comment #68
nikhil_110 commentedAdded Re-roll patch #60 on Drupal 9.5.x
Comment #69
fgm(Naughty bug removed my credit for the original patches)
Comment #71
rolodmonkey commentedFound a related issue in the Drupal 8+ migration (source) module:
#3184165: serialized values becomes strings
Comment #72
marcusml commentedRestoring issue summary, tags and related issue which was removed in #68.
Comment #74
vsujeetkumar commentedRe-roll patch needed for 11.x.
Comment #75
vsujeetkumar commentedRe-roll patch created for 11.x.
Comment #76
mradcliffeI added the Needs issue summary update tag because it would help to understand remaining work and the proposed resolution specifically the test coverage expansion in the issue summary rather in the comment chain.
I am working with @ompiepy at MidCamp 2024 and we're going to look at this issue to try to move it forward. Possibly transitioning to a merge request after updating the issue summary.
Comment #77
ompiepy commentedI am looking into the issues to get the summary update and then start working for the merge request.
Update: I am done for the day. But, I will try to update and love to help. Slack: Om Prakash Sharma
Comment #80
samitk commentedComment #81
smustgrave commentedDid not review
Issue was tagged for issue summary update which appears to still be needed.
Comment #82
mdsohaib4242 commentedTo handle the error when $options is NULL in buildUrl(), you can add a simple check to ensure that $options is always an array.
Something like this
Comment #83
djg_tram commentedThe patch we suggested seven years ago (!), simply casting to array, was already capable of accomplishing this. :-)
I have long moved on since and no longer bitten by this problem but this one is really ridiculous, I have to say. A very straightforward modification that obviously doesn't even require testing, it cannot cause any problems or regresssion by definition, and nobody ever bothered to accept it (or even argue against it). Must be one of the record setters in Drupalland. :-)
Comment #84
hudriIn response to #3 and #76:
The options of an URL object are documented as optional, which means NULL is a valid value.
So it does not really matter where the underlying problem is coming from. The problem is that the code blindly assumes that $options is a mandatory array, which does not comply with the spec. So any code in core that assumes that options is array, should be rewritten to be null-safe.
I got into this ticket due LinkGenerator, which has exactly the same incorrect not-null assumptions (line 93 and line 154)
So IMHO the underlying problem is not that somwhere the options aren't set, the underlying problem is that this code is not following the specification. I've added this to the issue summary.
Comment #85
hmdnawaz commentedpatch for 11.2 without tests
Comment #86
longwave> The options of an URL object are documented as optional, which means NULL is a valid value.
It doesn't mean NULL is valid, because the default $options is an empty array. Eventually we will be adding types and this argument will be typed as an array.
This means it is the caller's responsibility to ensure an array is passed in. In the LinkFormatter case, the underlying
optionsproperty in LinkItem is a Map, which is stored either as an array of values or NULL. Therefore I think the fix should be inLinkFormatter::buildUrl():Comment #87
dcam commentedI did a deep dive on this issue and discovered problems with the assumptions and the work that has been done so far in this issue. I'm fortunate to be debugging this with later versions of PHP that provide better error messages. The issue summary has been properly updated with detailed descriptions of the problems at hand.
The first important thing to understand is that
LinkFormatteris not receivingNULLas the options value. It's actuallyFALSE. As far as I can tell it has always beenFALSE. In fact, none of the above patches that explicitly treated the value as beingNULL, i.e. corrected it with the NULL coalescing operator, were reported as working. The only "working" patches are ones that just happen to correct theFALSEcase.The second important thing to note is that this really isn't an issue with the formatter. Any attempt to correct the problem in it will only cause the unserialization warnings to become more apparent. Then eventually
unserialize()will start throwing errors instead of warnings and we'll be back to having WSoDs again. The inherent problem is lower-level than that. We allow a field column to be NULL, but the DB layer always tries to unserialize the value. I don't know that this is even the DB layer's fault. It may be the field's for not requiring it. I have to solicit opinions about how it should be fixed.Comment #88
dcam commentedPostponing on #3300404: Handle nullable serialized field columns which should correct the unserialization problem. If the proposed fix for that one is implemented, then the options value will actually be NULL and we can proceed from there.
Comment #89
dcam commentedThis is unblocked now.
Comment #92
dcam commentedComment #93
dcam commentedComment #94
smustgrave commentedPretty straight forward fix
Shows test coverage, good work keeping it to a kernel test btw.
Comment #97
catchThanks @dcam for digging into the actual issue here and figuring out the correct fix.
@djg_tram you repeatedly demanded that an incorrect fix that wouldn't have solved the bug for many cases be applied, and by adding pointless noise to the issue personally contributed to it taking longer to fix than it should have. Shouting at everyone else to fix it faster does not actually help.
Committed/pushed to 11.x, thanks!
Comment #99
catch