Problem/Motivation
Coincidentally, this revealed that
ResourceResponse
still contains the data it must serialize! And so, whenever (Dynamic) Page Cache retrieves its cache items, the cache system unserializes the response, and so it must also unserializeResourceResponse::$responseData
.
This was the case ever since #1834288: RESTfully request an entity with JSON-LD serialized response, where RequestHandler
calls setContent()
on a ResourceResponse
. This now lives at \Drupal\rest\RequestHandler::renderResponse()
, but the basic principle is still the same. And that's the thing: the response will always have its response body set, there's no need for it to retain the object that was serialized into the response body.
Proposed resolution
Move the existing logic to take a ResourceResponse
and generate a response body from RequestHandler
(a controller) to ResourceResponseSubscriber
(a KernelEvents::RESPONSE
event subscriber).
This issue is not criticism on the REST module. It's just moving code to a response event subscriber, which is "the Symfony HttpKernel thing to do", i.e. it's how this sort of response manipulation is intended to work. Because this code was written early in the D8 cycle, this code was never updated to do this. So this patch does that.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
Comment | File | Size | Author |
---|---|---|---|
#22 | 2807501-22.patch | 40.15 KB | Wim Leers |
Comments
Comment #2
Wim LeersComment #3
Wim LeersComment #4
neclimdulSuper nice numbers. What happens when someone calls getResponseData() on an unserialized result though?
Comment #5
Wim LeersThat won't work. But why would code do that? Once the response has been determined, there's no need for that data to linger.
What's really happening here, is that
RequestHandler::renderResponse()
behaves as a response subscriber. It should transformResourceResponse
to a(Cacheable)Response
object, so that Page Cache & Dynamic Page Cache cannot (un)serialize this data unnecessarily.Comment #6
Wim LeersNote that this technically breaks BC, because code may be extending
RequestHandler
(see #2718545: Refactor \Drupal\rest\RequestHandler::handle() into smaller methods). But as of right now, thejsonapi
contrib module doesn't actually rely on overridingrenderResponse()
: http://cgit.drupalcode.org/jsonapi/tree/src/RequestHandler.php?id=id=2de....Plus, it's very very likely to be the only contrib module, and it's targeted for core inclusion, so the risk in refactoring this code away is absolutely minimal.
Here's that alternative implementation. (Same performance numbers, because effectively the same data ends up in Page Cache.)
Comment #7
Wim LeersComment #9
Wim LeersI forgot to migrate the status code. Easy fix. I should've run tests locally, then I would've caught this.
Comment #11
dawehnerGiven we are in a subscriber and not inside the routing layer any longer, I~m wondering whether using
\Drupal\Core\Routing\RouteMatch::createFromRequest
would be safer here.I'm wondering whether having some direct documentation here pointing towards the subscriber would make it clearer what is going on
Comment #12
Wim LeersThat assertion I included in the patch in #6 was well-intended, but it's sadly wrong. Removed that.
Also, I moved a bunch of code from
RequestHandler
toResourceResponseSubscriber
, but then did not update the kernel test coverage ofRequestHandlerTest
to move that to aResourceResponseSubscriberTest
. Did that.Finally, I want to clarify that this issue is not criticism on the REST module. It's just moving code to a response event subscriber, which is "the Symfony HttpKernel thing to do", i.e. it's how this sort of response manipulation is intended to work. Because this code was written early in the D8 cycle, this code was never updated to do this. So this patch does that.
In dong so, I converted it from a
Kernel
test to aUnit
test.(Note that locally I'm running into bugs with
run-tests.sh
failing on PHPUnit tests with lots of@dataProvider
test cases. I sure hope that testbot won't have the same problem… because with thephpunit
test runner, it works just fine. The mere act of inserting the results into the database is what fails, because the column length is exceeded.)Comment #13
Wim Leers#11:
@see
on thehandle()
method's docblock. Happy to add it in the code too if you want. But this is nothing special, really. The same already happens for HTML responses, AJAX responses, and so on.Comment #15
tstoecklerThis patch makes a lot of sense. I learned a lot from just reading it, thanks!
Some notes:
Minor, but seems unnecessary to return the response, when it's being passed in and modified.
Without looking at the docs I would guess the return value of a method called
renderResponseBody()
to be a$response_body
, not the$response
. So not returning anything would be clearer in my opinion. I realize, that that's arguable, though, so feel free to leave as is, as well.This is pretty nifty! Nice work.
I looked at Symfony's
Response
and besides the stuff that is being taken care of here, there is the following state in a response object:version
(the HTTP version),statusText
, andcharset
.I can't actually think of those ever being very useful (maybe
statusText
, but I think we should take those over as well, just for completeness' sake.:-)
Double newline
Am I missing or are these assertions pretty pointless?
Seems there is no test coverage for uncacheable responses?
Not sure how hard that would be to integrate, though.
Minor, but in my opinion the class would read more fluently if this were the last method.
Wow
Unused here, I think.
Comment #16
Wim LeersstatusText
, because that is set automatically based on the status code, see\Symfony\Component\HttpFoundation\Response::setStatusCode()
.ResourceResponseSubscriber
would not run certain bits of code. Then further down we assert that the returned response no longer implements one of the interfaces. So while it indeed is not super helpful test coverage itself, it makes the overall test coverage more clear IMO.\Drupal\Tests\rest\Kernel\RequestHandlerTest
. But I of course agree that it would be better to have proper full unit test coverage while we're touching this code anyway. So, added that.Comment #17
Wim LeersAnd this then addresses #15.7.
Comment #18
tstoecklerThank you, the fixes look great. Also looked through the patch again, and - again - found it very readable and a great cleanup. I'm general not an expert on the request processing system, but this A) just makes sense all around B) @dawehner and @neclimdul looked at it and didn't find it too offensive and C) is mostly moving code around, so I'm going to be a tad bold and RTBC this one.
Comment #19
Wim LeersThanks for the review and praise!
Comment #20
alexpottI think we should prefix the service name with
rest.
- whilst it is not a standard - it should be and definitely will make this addition less likely to clash.There are now some unused uses in this test.
Comment #21
dawehneri wonder whether we could describe better what this class is doing
I'm wondering whether we could put the if into the if($context->isEmpty()) bit, so we keep just one $serializer->serialize() call
Is it just me, or is this kinda sad? We mostly copy an existing response, right? I'm a bit confused about the function name.
nitpick: We could use
onResponse
Did you considered to use named data provider, but yeah, meh
Comment #22
Wim Leers#20:
html_response.subscriber
andajax_response.subscriber
incore.services.yml
. Still, I agree this is better.#21:
\Drupal\Core\EventSubscriber\HtmlResponseSubscriber
and\Drupal\Core\EventSubscriber\AjaxResponseSubscriber
. But, improved it since you asked.ResourceResponse
that's been there since the very beginning: we're ensuring that the response we send (to the end user, but hence also the the decorating middlewares) no longer contains the arbitrarily complex PHP object that it can contain inResourceResponse::$responseData
. The interface nor the implementation allow us to remove that data/object. So, we have no choice but to clone everything of an existing response except for this object. The removal of this object can be considered flattening, hence the name.Added some docs to clarify this.
Comment #23
dawehnerWell, if we don't start now we never start.
Mh sure, well, I try to improve things :)
Comment #24
alexpottI'm pondering about the BC implications of this change. The only usage in core is \Drupal\rest\Plugin\ResourceBase::getBaseRoute() as the controller class. We've already said that controllers are internal (see https://www.drupal.org/core/d8-bc-policy#controllers). So I think this change is allowed.
Comment #25
alexpottCommitted 0f3f279 and pushed to 8.3.x. Thanks!
Comment #27
Wim Leers#23: yep! I'm just explaining the rationale behind the current code. I did agree those were all improvements, hence I made all those changes :)
Comment #28
Wim LeersBTW, this triggered me to do more research. This made anonymous REST requests/responses 50% faster. But what about authenticated ones? For that, see #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster.
Comment #29
Wim LeersAlso, I think a 60% performance/scalability boost (see #2) for anonymous REST requests is worth a mention in the 8.3.0 release notes?