-
Notifications
You must be signed in to change notification settings - Fork 11.2k
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
[9.x] memory exhausted because JSON payload is unwanted parsed twice by Illuminate\Http\Request::createFromBase #42403
Comments
You can remove your If you have to do that, you should do it in your custom request (or create one with you don't use one), by overriding the And for the underlying issue, in my testings, json() is called once by createFromBase : clean function does seems to do extra calls to json though |
@IK77 framework/src/Illuminate/Http/Request.php Lines 374 to 376 in 465dbee
So the subsequent calls doesn't call @MircoBabin the change introduced in 13e4a7f Replaced this call: framework/src/Illuminate/Http/Request.php Line 435 in 76b3417
By this: framework/src/Illuminate/Http/Request.php Lines 458 to 460 in 465dbee
So, before it called framework/src/Illuminate/Http/Request.php Lines 390 to 397 in 465dbee
Bottom line: I think it is very unlikely this behavior will be changed by maintainers after so many years. You could take @IK77 suggestions and either use the already decoded data, or if really want to decode it yourself, maybe to use different parameters on
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(\Illuminate\Http\Request::class, \App\Http\Request::class);
}
public function boot()
{
}
} Note, while framework/src/Illuminate/Foundation/Providers/FormRequestServiceProvider.php Lines 33 to 37 in 465dbee
Hope this helps you solve your issue. |
@driesvints after doing research to answer @MircoBabin , I noted PR #37921 , which introduced the check if a request is a JSON request before replacing its Actually the tests (one changed, and one added) on that PR, both passes if one reverts the change made by it. I am not sure it makes any change if reverted, as for non-JSON requests I have no idea on how to do so, but maybe there is a way to automate to check if any added tests fail before committing a PR's proposed change? |
Thanks @rodrigopedra. @LukeTowers could you weigh in here? |
I have taken @IK77 advice and do not json_decode() myself anymore. Instead i'm using Request::json() now. But still I'm getting memory exhausted because Laravel is decoding twice. Here is new the profile: |
@rodrigopedra thanks for you detailed explanation. So it seems I'm wrong concluding 13e4a7f was the commit introducing wrong behaviour. Your solution to extend the Walking this path it would be easier to just directly modify the |
To be fair, isn't the real issue that your JSON payloads are simply too large? What kind of payloads are you handling? |
The payloads are order request payloads for renting a touringcar. In the payload also is the calculated Google Maps route present. That together constitutes a lot of bytes. The http-payload is ~7 MB, which is not that big. Json_decoded() it becomes ~69MB, which still is not beyond default PHP limits. But then comes Laravel, deciding to decode twice and keeping 2 copies in memory. So claiming 2 times 69MB which is beyond the default PHP limits. |
If you have a solution to this issue that does not break other use cases please PR it. Thank you. |
@rodrigopedra thanks for your detailed response! My change however is absolutely critical, although it might not appear so at first glance which is why the original behaviour was so dangerous. The problem is that the internal Ultimately there are two things to keep in mind:
|
@MircoBabin where is Laravel calling json_decode twice? I'm not seeing it in the source. |
@LukeTowers gotcha. Thanks for the heads up. My misunderstanding was due to Still, I found weird the added test passed if the change is reverted. Maybe is it worth adding another one that fails without the change? |
@MircoBabin , I am not familiar with the call trace visualization tool you are using, but comparing the two screenshots you posted, it is not clear to me, in the last one, that I apologize in advance if it is a lack of familiarity with the tool. On the other hand, if you can provide a clearer spot where |
@rodrigopedra I also find it odd that the test passed without the change applied, when I made the PR the test failed without the change. |
@rodrigopedra @LukeTowers I have adjusted /**
* Get the JSON payload for the request.
*
* @param string|null $key
* @param mixed $default
*
* @return \Symfony\Component\HttpFoundation\ParameterBag|mixed
*/
public function json($key = null, $default = null)
{
if (!isset($this->json)) {
static $calledCount = 0;
++$calledCount;
file_put_contents('c:\\incoming\\out-of-memory.'.$calledCount.'.log', (strval(new \Exception('\Illuminate\Http\Request.php function json() is called - executing json_decode()'))));
$this->json = new ParameterBag((array) json_decode($this->getContent(), true));
}
if (is_null($key)) {
return $this->json;
}
return data_get($this->json->all(), $key, $default);
} 2 files are written:
|
The problem is in /**
* Get a cloned instance of the given request without any trailing slash on the URI.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Request
*/
protected function requestWithoutTrailingSlash(Request $request)
{
$trimmedRequest = Request::createFromBase($request);
$parts = explode('?', $request->server->get('REQUEST_URI'), 2);
$trimmedRequest->server->set(
'REQUEST_URI', rtrim($parts[0], '/').(isset($parts[1]) ? '?'.$parts[1] : '')
);
return $trimmedRequest;
}
Adjusting /**
* Create an Illuminate request from a Symfony instance.
*
* @return static
*/
public static function createFromBase(SymfonyRequest $request)
{
$newRequest = (new static())->duplicate(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), $request->files->all(), $request->server->all()
);
$newRequest->headers->replace($request->headers->all());
$newRequest->content = $request->content;
if ($newRequest->isJson()) {
if ($request instanceof self) {
$newRequest->request = $request->json();
$newRequest->json = $request->json; // Unsure if this is allowed, because this is a ParameterBag
} else {
$newRequest->request = $newRequest->json();
}
}
return $newRequest;
} |
@MircoBabin can you test changing the first line of from: $trimmedRequest = Request::createFromBase($request); to: $trimmedRequest = $request->duplicate(); And see if it fixes it? |
@rodrigopedra I can confirm after changing |
@MircoBabin I sent PR #42420 to make this change. Thanks for tracing the spots so we could find where the double encoding happened =) |
@rodrigopedra The PR targets the [8.x] branch. Can it also target the [9.x] branch ? Because that's the version I'm using. Or must the PR first be accepted on the [8.x] branch and will then later be targeted at the [9.x] branch and future [10.x] branch ? Sorry for the question, I don't know how the Laravel project is handling patches on multiple versions. |
@MircoBabin all fixes for 8.x are merged upstream to newer versions. |
@MircoBabin as it is a bugfix it should target the 8.x, as this branch is still in bugfix support. Complementing what @driesvints said, changes in 8.x are merged into 9.x before each weekly release. |
Thank you very much ! |
@MircoBabin excellent work tracking down the issue and great final solution @rodrigopedra! Glad to see the issue resolved with a general win for Laravel performance :) |
Will be fixed in the next release. Thanks all. |
Description:
I have an api POST route with json contents. Very unexpected this route was giving Allowed memory size of 134217728 bytes exhausted (tried to allocate ... bytes) errors.
To debug this issue:
I attached XDEBUG in profile mode.
I replayed the request.
I used QCacheGrind to visualize the profile:
As the profile shows,
Illuminate\Http\Request::createFromBase
viaRequest->json()
callsjson_decode()
twice. Which is unwanted, because I don't use theRequest::json()
to retrieve the json contents. I do my ownjson_decode((string) Request::getContents())
.I did some digging in the Laravel History and found the commit introducing the json_decode behaviour in [9.x]: 13e4a7f The commit message states: ... This commit solves the underlying issue without breaking compatibility with the original functionality. ...
This commit is breaking compatibility, because it does not take memory consumption into account. Neither does it take performance into account, decoding the same request body twice is performance degradation. For large JSON (in my case Content-Length: 6428178) this can easily lead to memory exhausted errors. In Laravel 7.x there was no memory exhausted error.
The text was updated successfully, but these errors were encountered: