Problem/Motivation
Since June 9, the updated dependency environments are failing across PHP versions in
Drupal\Tests\migrate\Functional\process\DownloadFunctionalTest:: testExceptionThrow()
:
https://www.drupal.org/pift-ci-job/2400269
The fail is consistent across PHP versions in the 9.x branches. (It may or may not also affect D10; we can't tell at present from the jobs against HEAD because of an unrelated PHPStan failure that aborts the test early.)
Proposed resolution
There are two problems here. The first (Comments 1-24) is that the exception message from Guzzle changed. The fix is to make the test less specific.
The second problem (Comments 15 and 25ff) is that BrowserHtmlDebugTrait::getResponseLogHandler()
tries to cast a Stream object to a string. This fails when the Stream object represents the destination file. The fix for that is to check isReadable()
.
Remaining tasks
- Upstream report needed? See #44.
Investigate why the (write-only) stream for the destination file is returned as the response body. See #15, #41.Follow up in #3292980: Testing system should explain why Guzzle responses can be unreadable.
User interface changes
N/A
API changes
N/A
Data model changes
N/A
Release notes snippet
TBD
Comment | File | Size | Author |
---|---|---|---|
#45 | upstream-psr7-3285230-10.0.x-44.patch | 2.49 KB | xjm |
#43 | upstream-psr7-3285230-fix-only-43.patch | 2.5 KB | benjifisher |
#43 | interdiff-3285230-29-43.txt | 1.67 KB | benjifisher |
#42 | upstream-psr7-3285230-fix-only-42.patch | 2.7 KB | benjifisher |
#40 | upstream-psr7-3285230-fix-only-10.0.x-36.patch | 2.97 KB | xjm |
|
Issue fork drupal-3285230
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
xjmIt's worth noting that, while the start of this failure is timed specifically close to the recent Guzzle release, the test does not fail on the normal environments, and the Guzzle update has been deployed already there. So this is due to something else, unless Guzzle 7.4.4 breaks D9 in a way that 6.5.7 7.4.3 don't, and that doesn't break D10.
Attached updates 9.4.x to the latest versions of dependencies allowed by core constraints. Theoretically, this should fail on the normal test environments with the same failure that's linked in the IS.
Comment #4
xjmJust noticed that the updated deps environments are passing on 9.3.x, which does implicate Guzzle 7.4.4. Here's a patch to test that hypothesis instead.
Comment #6
xjmOK, so it's not Guzzle... let's proceed with the process of elimination from there.
Comment #7
xjmWait, this doesn't look right. I guess we only have the direct constraint for it in Drupal 10, versus it being an indirect dependency in D9. Let's test that first before ruling it out.
Comment #8
xjmUpgrading both Guzzle and the PSR-7 package.
Comment #10
xjmAha! It's the PSR-7 package.
D10 is pinned to 2.2.1, so here's a patch to see whether the test is also broken on D10 with the update.
Comment #11
xjmComment #13
xjmSo yep, this test is broken on D9 and D10 both with
guzzlehttp/psr7
2.3.0.Comment #14
xjmThe D10 test has a more comprehensible failure, which might be a good place to start:
Comment #15
quietone CreditAttribution: quietone at PreviousNext commentedThis has to do with this change, Safely get stream contents. There is now a check that a stream is readable before attempting to get the contents. Well, that makes sense. What doesn't make sense is that it is failing when trying to read the destination, which should only be written to.
If I change the mode of the destination to be w+' instead of 'w', (and make adjustments to the exception message) this will pass.
Why does guzzle think it needs to read the destination?
Comment #16
xjmComment #17
phenaproximaI haven't dug too far into this but my feeling, based on looking at the failed assertion @xjm posted in #14, is that the test is overzealous. It looks like it should be asserting a simple 404, but is asserting the entire exception message, which inherently ties it to how Guzzle internally wants to work. I'd suggest adjusting the test expectation so it's looking for a 404 status code, regardless of whatever the actual message is.
Comment #18
xjmSo the assertion is:
From hereabouts:
Testing the specific message does seem a bit like testing Guzzle.
However, there isn't any way I can see to check the status code of the response. I didn't walk through it in a debugger or anything, but I suspect this is happening when
MigrateExecutable
catches\Exception
and handles it thus:The full
\Exception
object isn't retained in any form that I can see, so there's no way to check the status code that I can tell without changing either the above method orDrupal\Core\Utility\Error
.Comment #19
phenaproximaI'd probably just assert the presence of
404 Not Found
in the exception message with$this->assertStringContainsStringIgnoringCase()
.Comment #20
xjmBut that's not present in the message at all now.
Comment #21
xjmMaybe just this.
Comment #22
phenaproximaAh yes, that's true. I must have been malcaffeinated when I wrote #19.
Another option, then, is change
handleException()
to include the exception code in the logged message, along with some hard-coded prefix. For example, assuming a 404 error has a code of 404:Then in the test you could have something like
$this->assertStringStartsWith('[Guzzle] Error 404:', $message);
.Comment #24
xjm#22 does not work -- it is catching
\Exception
so the error could be anything. We could however include the type of that exception by getting the class, but that has also changed with the upstream release I think.Meanwhile patches NW and I must've had a stale 9.4.x branch.
Comment #25
xjmPlot twist...
That integer integer-y thing means the migration actually failed once the test gets past the status message, so our approach of being more permissive with the message doesn't fix it because it's sadly apparently actually broken.
Comment #26
phenaproximaSelf-assigning to smash this bug.
Comment #27
benjifisherThe attached patch is obviously not the right fix, but it points out where the problem really is.
Short version: the test is broken.
We had a rollicking discussion about this in Slack today, mostly during the weekly migration meeting. I will summarize the discussion later. (Time for supper.)
Also, does someone want to open a separate issue for this? There are two very different problems here: the changed exception message because of the Guzzle update and then the broken attempt to cast a stream to a string so that we can log it.
Comment #28
benjifisherComment #29
benjifisherI was in a hurry to get supper ... pretend you never saw the files attached to #27.
Comment #31
xjmExtracting fix from diff. :) And crediting folks.
Edit: And crossposting apparently.
Comment #32
xjmComment #33
xjmComment #34
xjmFWIW I'm okay with including these two changes in the same issue scope.
Comment #35
mikelutzHate to send a critical back for a nit, but this comment is now nonsensical
- edit Maybe not nonsensical, since the silence is still there a few lines down, but I also question if the silence is still necessary if we are no longer casting a stream.
Comment #36
xjmRe: #35 is that in scope though? It's still technically true on 9.x, and out of scope on 10.x.
Comment #37
mikelutzLeaving at NR, the comment could be updated, but It's not as off as I first thought, and I really don't know whether the silence is still needed or not.
Comment #38
mikelutzYeah, I'll agree with out of scope for this critical. RTBC pending tests.
Comment #39
mikelutzLooks like 10.0 needs a re-roll
Comment #40
xjm10.0.x patch, and a stab at making the comments more accurate. Turns out the bit about PHP 7.4 was already removed. No interdiff 'cause
git apply --reject
.Edit: And more xposts.
Comment #41
benjifisherThanks to @quietone for #15 (and to @xjm for bringing that comment to my attention):
Good question!
I hacked the exception message in Guzzle, and that confirmed what @quietone said: Guzzle was calling
getContents()
on the destination stream. Then I hacked thedownload
process plugin to add a stack trace to its exception message:So it seems that
BrowserHtmlDebugTrait::getResponseLogHandler()
is trying to cast a Stream object to a string:The comment, and the cast, was added in #3086374-79: Make Drupal 8 & 9 compatible with PHP 7.4. I am adding that as a related issue.
Comment #42
benjifisherWhile you were busy cross-posting, I was checking the return type of
getBody()
. Like @mikelutz, I think the comment should be removed.Comment #43
benjifisherWe do not need that
use
statement any more.Comment #44
benjifisherMaybe we should raise an upstream issue with Guzzle. According to the API docs for Psr\Http\Message\StreamInterface::__toString,
Comment #45
xjmMuch nicer, thanks @benjifisher. D10 version of above.
lol xposts. I think an upstream issue is good too; we could add an @todo I suppose linking an upstream PSR. I would still commit this today though because HEAD is busted on the secure version of Guzzle.
Comment #46
mikelutzBack to RTBC pending tests.
Comment #47
benjifisherNow that we understand the problem better, let's update the IS.
Comment #51
xjmPhew!
Comment #52
xjmTagging for follow up in case we want to file an upstream issue with guzzle.
Comment #53
benjifisherI added #3292980: Testing system should explain why Guzzle responses can be unreadable as a follow-up issue, so I am updating the "Remaining tasks" in the issue summary. I also copied the other task to the new issue.
Comment #54
benjifisherOops:
should have been
Should we add a commit to this issue or make a follow-up issue?
Comment #55
mikelutzMake a novice issue, let somebody get credit for it. If it survives the night I'll have let someone on my team get their first core credit tomorrow.
Comment #56
mikelutzI opened #3293114: Fix var tag on recent commit, and am going to try to walk someone on my team through their first contribution this morning.
Comment #57
mikelutzClosing a few loops on this issue.
Regarding
I posted a fairly detailed explaination here, but the short of it is that guzzle by default takes the download stream, dumps it into a sink stream and returns that stream for $response->getBody(). There is a guzzle option ($options['stream'] = TRUE) that would return the download stream instead, but it's rarely used, and certainly not by us here. Guzzle uses a php temp stream opened 'r+' (read/write) by default, and we overrode it to a write- only file stream, so it's not doing anything unusual, we just overrode the typically readable stream with a write-only stream.
Additionally:
We discussed a follow up to report the psr 7 violation of Stream::__toString throwing an exception in violation of psr7 StreamInterface. I dug in through the history, and the short version is they had a discussion and intentionally chose to throw the exception anyway.
The slightly longer version is that psr-7 was accepted in 2015, back in the php 5.6 days. Per the php docs for __toString()
Guzzle chose to interpret
.
to mean that they weren't permitted to throw an exception prior to php 7.4 due to php throwing a fatal error, but after 7.4 they were permitted to throw an exception. There was discussion of attempting to file an errata with PHP-FIG to change psr7, not sure if they did, but they didn't have much hope of getting it updated. Hence why the code in __toString catches exceptions and only rethrows them in php 7.4.
On top of that discussion which was a couple years ago, and only threw the exception on a detached stream, https://github.com/guzzle/psr7/pull/515/files mentioned above expanded the situations where an exception is thrown, including unreadable streams.
Further reading:
https://github.com/guzzle/psr7/issues/271
https://github.com/Nyholm/psr7/issues/142
We've also opened #3293114: Fix var tag on recent commit (committed) to fix the @var typo, and #3292980: Testing system should explain why Guzzle responses can be unreadable(RTBC) for further discussion and to update some documentation
Since it seems pointless to follow up upstream on the PSR violations. and the other internal follow-ups have been filed, I'm removing the Needs Followup tag.