Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix hasFile Method to Accurately Identify Files in Request #52445

Draft
wants to merge 2 commits into
base: 11.x
Choose a base branch
from

Conversation

alirezadp10
Copy link
Contributor

Summary

This pull request addresses an issue in the Illuminate\Http\Client\Request class where the hasFile method could incorrectly identify non-file data as a file when checking for multipart form data. The fix enhances the logic in the hasFile method to ensure that only actual file uploads are recognized as files, preventing false positives.

Problem

The hasFile method relied on checking whether the contents of a request's data matched certain criteria, but it lacked a robust mechanism to differentiate between actual file uploads and plain string data. This led to scenarios where non-file data with the same key as an expected file could be misinterpreted as a file, resulting in inaccurate behavior.

Solution

The proposed fix introduces the following improvements:

  • Resource Check: The method now includes a check using 'is_resource($file['contents'])' to determine if the 'contents' of the request data are a valid file resource (e.g., a file handle).
  • Enhanced Validation: The existing 'json_encode($file['contents']) !== false' check is retained to cover binary data scenarios.
  • Test Coverage: New test cases have been added to verify that 'hasFile' correctly distinguishes between non-file data and actual file uploads, ensuring that false positives are avoided.

Impact

  • Backward Compatibility: This change does not introduce any breaking changes and is fully backward compatible.

  • Accuracy Improvement: The method now more accurately identifies file uploads, leading to better reliability in handling multipart form data in HTTP requests.

@alirezadp10 alirezadp10 force-pushed the alirezadp10/fix-has-file-issue branch from 58ad393 to bc8306d Compare August 9, 2024 16:15
@@ -666,11 +666,28 @@ public function testFilesCanBeAttached()
$this->factory->assertSent(function (Request $request) {
return $request->url() === 'http://foo.com/file' &&
Str::startsWith($request->header('Content-Type')[0], 'multipart') &&
$request[0]['name'] === 'foo' &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this test changed?

I understand the premise of the PR, but this test case is named "files can be attached", and the condition to check for an attached file is removed?

In the best case, no previous test cases should be changed, but again, I see from your proposed changes why you did it.

Either this test case should be renamed or removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, we check some of the other assertions related to attaching a file in a request, while in the new test, we focus solely on the functionality of the hasFile method itself. In my opinion, these tests complement each other and test different aspects of the framework. However, they could be merged into a single test that checks the entire process of attaching files.

@@ -149,7 +149,8 @@ public function hasFile($name, $value = null, $filename = null)
return collect($this->data)->reject(function ($file) use ($name, $value, $filename) {
return $file['name'] != $name ||
($value && $file['contents'] != $value) ||
($filename && $file['filename'] != $filename);
($filename && $file['filename'] != $filename) ||
! is_resource($file['contents']) && json_encode($file['contents']) !== false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the previous checks are using OR (||), shouldn't these last two be wrapped in parentheses?

Also, in your description you mention the json_encode() is "retained", but the GIT diff doesn't show it on the previous version...

  • Where did it come from?
  • Isn't trying to encode every attachment as JSON, expecting it to fail, as a condition inside a Collection@reject() callback, an expensive operation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding your first point, I see what you mean, and I will use parentheses for that. However, regarding your second point, there was a misunderstanding in my explanation. I intended to use json_encode to ensure that we only check the file attachments rather than other fields in the request body, and I didn't mean to imply that this function was already used.

This approach enhances the logic of the code by ensuring that we check whether the request contains a specific file, which is the expected functionality of this method.

@rodrigopedra
Copy link
Contributor

I am not sure if I follow the reasoning.

The component being changed is the lluminate\Http\Client\Request, which allows sending requests out of the application, right?

Currently, if one needs to send a simple string as an attachment, per a 3rd party API constraint, for example, one could just call PendingRequest@attach() with a string (usually proxied by the factory object), and if needed use hasFile() to check if the pending request, has a pending file attachment, right?

If this PR gets merged, one would need to create either a real or a temp file, and attach its handler, to have the hasFile() working? For a request that is pending to be sent later by Guzzle?

What is the actual benefit?

@rodrigopedra
Copy link
Contributor

Although there is no change in the method signature, the behavior is hugely changed, IMO it is a breaking change.

It is not an improvement per se, or a bug fix, but a behavior change. It might be the desired behavior, and if so, should be merged. But nonetheless, it seems like a behavior change that can cause previous code to stop working.

@alirezadp10
Copy link
Contributor Author

I am not sure if I follow the reasoning.

The component being changed is the lluminate\Http\Client\Request, which allows sending requests out of the application, right?

Currently, if one needs to send a simple string as an attachment, per a 3rd party API constraint, for example, one could just call PendingRequest@attach() with a string (usually proxied by the factory object), and if needed use hasFile() to check if the pending request, has a pending file attachment, right?

If this PR gets merged, one would need to create either a real or a temp file, and attach its handler, to have the hasFile() working? For a request that is pending to be sent later by Guzzle?

What is the actual benefit?

Thank you for your questions and for taking the time to review the proposed changes.

You're correct that Illuminate\Http\Client\Request is responsible for sending requests out of the application. The hasFile() method is designed to check if a request contains a file attachment, which is useful in certain scenarios, especially when interacting with APIs that require file uploads.

The problem with the current implementation of hasFile() is that it does not accurately distinguish between actual file uploads and simple string data. For instance, if a request contains a string value that is expected to be a file, hasFile() might incorrectly return true, even though the string is not a file. This could lead to unexpected behavior, especially when the application or a third-party API expects a real file upload.

The key benefit is that this change improves the accuracy and reliability of the hasFile() method. It eliminates the risk of false positives when dealing with file uploads, ensuring that the method only confirms the presence of real files. This is particularly important when working with APIs that have strict file upload requirements.

In scenarios where you're attaching a string due to third-party API constraints, this change encourages more explicit handling of file uploads by requiring a file resource. This makes the code more predictable and reduces the potential for errors when working with file uploads in HTTP requests

@alirezadp10
Copy link
Contributor Author

Although there is no change in the method signature, the behavior is hugely changed, IMO it is a breaking change.

It is not an improvement per se, or a bug fix, but a behavior change. It might be the desired behavior, and if so, should be merged. But nonetheless, it seems like a behavior change that can cause previous code to stop working.

You’re correct that this could be considered a breaking change since it modifies how hasFile() interprets what constitutes a file. Code that previously passed a string and expected hasFile() to return true might now fail unless an actual file resource is used.

The primary reason for this change is to improve the accuracy of the hasFile() method in identifying genuine file uploads. The current behavior can lead to false positives, which could result in bugs, especially when interfacing with APIs that strictly require files and not strings.

In conclusion while the change is intended to improve the robustness and accuracy of file handling, it’s clear that this could impact existing applications that rely on the current behavior. If this is the desired direction, we should clearly communicate the change to users and consider the best way to introduce it without causing disruption.

@rodrigopedra
Copy link
Contributor

The problem with the current implementation of hasFile() is that it does not accurately distinguish between actual file uploads and simple string data. For instance, if a request contains a string value that is expected to be a file, hasFile() might incorrectly return true, even though the string is not a file.

Sure, but if all one wants is to attach some string to be sent as a file, on a multipart request body, and later check if they have attached it, the current implementation has them covered up.

As this object is constructed by the application developer, and not by parsing external input such as its Illuminate\Http\Request counterpart, I don't see the benefit of the additional check, granted it still allows simple strings to be sent as file attachments.

But let's wait for the maintainers' verdict.

@taylorotwell
Copy link
Member

Can you describe what is actually going wrong in your production app with the current method and how to reproduce?

@taylorotwell taylorotwell marked this pull request as draft August 13, 2024 17:36
@taylorotwell
Copy link
Member

taylorotwell commented Aug 13, 2024

Please mark as ready for review when you want me to take another look.

@alirezadp10
Copy link
Contributor Author

Can you describe what is actually going wrong in your production app with the current method and how to reproduce?

sure, create a form with multiple non-file fields (e.g., text, arrays, or JSON data).
Submit the form without any actual file attachments.
Use Request::hasFile('some_field') to check for file uploads.
Expected outcome: Request::hasFile should return false when no files are uploaded.
Actual outcome: Request::hasFile returns true, incorrectly identifying non-binary fields as files.

@alirezadp10 alirezadp10 marked this pull request as ready for review August 14, 2024 13:53
@rodrigopedra
Copy link
Contributor

Can you show how to "create a form", for the HTTP Client component, with code?

@rodrigopedra
Copy link
Contributor

rodrigopedra commented Aug 14, 2024

Also, how are you accessing the Illuminate\Http\Client\Request instance? Since it is manually created internally by the Illuminate\Http\Client\PendingRequest instance before dispatching it to guzzle?

And therefore the Illuminate\Http\Client\Request@hasFile() is inaccessible? Since it is not exposed nor by the Illuminate\Http\Client\PendingRequest class, nor by the HTTP Façade?

Is it from a Guzzle Middleware?

https://laravel.com/docs/11.x/http-client#guzzle-middleware

@taylorotwell
Copy link
Member

Yeah a simple copy and paste reproduction would be helpful.

@taylorotwell taylorotwell marked this pull request as draft August 15, 2024 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants