Problem/Motivation
This issue is a follow-up to #3285230: Migrate's DownloadFunctionalTest:: testExceptionThrow() is failing on guzzlehttp/psr7 2.3.0.
The download process plugin in the migrate module uses Guzzle to copy a file from source to destination. When this is done in DownloadFunctionalTest::testExceptionThrow(), the testing system tries to save the HTTP request in case it is needed for debugging. For some reason, BrowserHtmlDebugTrait::getResponseLogHandler() tries to convert the destination stream, not the source stream, to a string. In #3285230, we handled that by testing isReadable() before casting the stream to a string.
For this issue, let's figure out why the testing system is trying to save the result of the destination stream instead of the source stream.
We should also figure out why some of us found that DownloadFunctionalTest::testExceptionThrow() passed locally but others got the same failure as the testbot.
Steps to reproduce
- Run DownloadFunctionalTest.
- Review the "HTML output was generated".
For example, using the current 10.0.x:
$ vendor/bin/phpunit -c core core/modules/migrate/tests/src/Functional/process/DownloadFunctionalTest.php
PHPUnit 9.5.20 #StandWithUkraine
Testing Drupal\Tests\migrate\Functional\process\DownloadFunctionalTest
. 1 / 1 (100%)
Time: 00:01.972, Memory: 6.00 MB
OK (1 test, 11 assertions)
HTML output was generated
https://drupal.ddev.site/sites/simpletest/browser_output/Drupal_Tests_migrate_Functional_process_DownloadFunctionalTest-17-79589012.html
https://drupal.ddev.site/sites/simpletest/browser_output/Drupal_Tests_migrate_Functional_process_DownloadFunctionalTest-18-79589012.html
Opening the second link in a browser, I see this:
ID #18 (Previous | Next)
---
Called from GuzzleHttp\Promise\FulfilledPromise::GuzzleHttp\Promise\{closure}() line 41
---
GET request to: http://web/core/misc/favicon.ico
---
Response is not readable.
---
The last line comes from the test we added in #3285230: it means that the stream $response->getBody() represents the destination of the download process, so it is not readable. The source stream is http://web/core/misc/favicon.ico, which is readable.
Proposed resolution
- Add a code comment explaining why the test for
isReadable()is needed.
Remaining tasks
- Upstream report needed? See Comment #3285230-44: Migrate's DownloadFunctionalTest:: testExceptionThrow() is failing on guzzlehttp/psr7 2.3.0.
According to the API docs for Psr\Http\Message\StreamInterface::__toString,
This method MUST NOT raise an exception in order to conform with PHP's string casting operations.
User interface changes
None
API changes
None
Data model changes
None
Release notes snippet
N/A
Issue fork drupal-3292980
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:
- 3292980-testing-system-tries
changes, plain diff MR !2469
Comments
Comment #2
benjifisherComment #3
benjifisherI repeated the test in the issue summary with Drupal 9.4.1:
This time, when I open the second link in a browser, I see this:
I think that just means that the stream is still unreadable, but the older version of guzzlehttp/psr7 casts it to an empty string. Before #3285230: Migrate's DownloadFunctionalTest:: testExceptionThrow() is failing on guzzlehttp/psr7 2.3.0, we used
@(string) $response->getBody()to cast to a string and suppress errors. (Does that suppress errors but not exceptions?)Comment #4
benjifisherAccording to @mikelutz on Slack:
Here is the code from the
downloadplugin:The next few lines may also be relevant here:Why do we close the stream and then return it? Is that related to the "HTML output was generated"?Edit: We return the string
$final_destination, not the closed stream, so there is nothing wrong with these lines.Comment #5
benjifisherIf the quoted explanation in #4 is correct, then the resolution to this issue might be simply to add a code comment. Something like this:
Is that accurate? Can we get a link to the Guzzle docs supporting this?
Comment #7
benjifisherI cannot find any documentation for how the 'sink' option affects the response, so I am adding a test to confirm the comment.
MR 2469 adds the test and a comment similar to the one in #5.
I am giving @mikelutz credit for the helpful discussion on Slack. See #4.
Is
Drupal\Tests\Core\Httpa good namespace for the new test?Comment #8
benjifisherComment #9
benjifisherComment #10
mikelutzThe code comment is fine, the test you added probably should not get committed.
You are just testing guzzle. I mean you could try to make an argument that you are testing \Drupal::httpClient() but all that does is return the http_client service from the container and the http_client service is GuzzleHttp\Client, so there is nothing there to really test.
Comment #11
mikelutzI got tired of guessing, so I decided to finally look it up.
It always dumps the download into a stream and returns that when you call getBody(). By default that stream is php://temp which is opened r+. The download is drained into the sink, the sink seek index is reset to 0 and that’s always what you get back. We are overriding that php://temp with a file stream that we opened 'w' instead of 'r+'.
The only way to get the actual download stream is to set
IF we opened the file 'r+' instead of 'w'.. then the response would act the same, and we probably wouldn't have noticed any difference in the behavior of $response->getBody().
There’s the meat from StreamHandler::createResponse(), but the good stuff is in createSink()
It opens a stream and drains the download into it and sets that as the response body either way. It’s not unreadable because we set a `sink`, it’s unreadable because we overrode the read/write sink guzzle would have used with something that we opened write-only.
I do really feel that the documentation in the ‘sink’ and ‘stream’ options sufficiently describes what’s happening here.
Comment #12
benjifisher@mikelutz:
Thanks for the review.
If anyone else is interested, @mikelutz and I discussed this issue at length during the #3294422: [meeting] Migrate Meeting 2022-07-07 2100Z.
I updated the MR, removing the test and changing the comment to this:
Part of our discussion is that Guzzle is not doing anything wrong, and our comment should not suggest that it is. In that spirit, I am updating the issue title.
Comment #13
quietone commentedI read the discussion in Slack today between @benjifisher and @mikelutz and agree with the analysis and conclusion.
This comment is definitely and improvement and is helpful.
Based on the discussion I am removing the second item, adding a test, from the proposed resolution section of the Issue Summary. While helpful for understanding this mikelutz points out that it is testing Guzzle not Drupal.
Now, setting to RTBC.
Comment #14
larowlanSaving issue credit. Crediting @mikelutz and @quietone for discussion here and on slack that went into resolving this issue.
Comment #15
larowlanCommitted to 10.1.x and backported all the way to 9.4.x for branch consistency as there's little risk of disruption here.