Problem/Motivation
PathValidator::getPathAttributes()
invokes the router with the global request context, does not use given request object for given path.
Menu links created from CLI are stored url-based instead route-base in the menu tree storage because of this.
Detailed analysis: see #2293697-157: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available.
Proposed resolution
Set the PathValidator
fake request object on the request context service, then restore the current request.
Remaining tasks
None.
User interface changes
None.
API changes
Data model changes
Comment | File | Size | Author |
---|---|---|---|
#40 | 2822190-40.patch | 3.67 KB | Wim Leers |
#8 | 2822190-8-testonly.patch | 1.75 KB | webflo |
#4 | 2822190-4.patch | 1.2 KB | webflo |
#2 | 2822190-2.patch | 647 bytes | webflo |
Comments
Comment #2
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedComment #3
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedComment #4
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedComment #7
dawehnerThat sounds like a usecase for finally somehow?
Comment #8
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedComment #11
dawehnerCan we inject the request context as constructor parameter?
Comment #12
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedIts possible to reuse the existing request context.
Comment #14
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedComment #15
dawehnerWhat about using
$old_request_context
and$request_context
or so, to distinct the two cases a bit better.Comment #16
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedAlright, lets go with
$initial_request_context
and$request_context
.Comment #17
dawehnerWFM
Comment #19
tstoecklerTestbot hiccup.
Comment #21
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedAnd again.
Comment #22
dawehnerAnd again
Comment #24
Wim LeersI ran into this same bug at #2293697-157: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available. You can see a full analysis there. I arrived at the exact same solution! So +1 for this patch.
Hence this is a blocker for a major bug. Updating issue metadata accordingly.
Comment #25
Wim LeersComment #26
dawehner@Wim Leers
This is not fair. Its Drupal which abuses things here.
Comment #27
Wim LeersIn #2293697: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available I wrote that I wasn't sure which one was wrong: Drupal or Symfony. Sometimes the line is difficult to draw.
So the WTF is then that Drupal exposes
RequestContext
as a service, even though it's meant to be a (mutable…) value object that one can create from a given request (and the current request can be retrieved using a service). Drupal exposing this as a service means you need to keep two services in sync: the request stack service (which is really meant to be a service) and the request context "service" (which is actually a value object, and was never meant to be a service).Comment #28
alexpottI don't understand why we're not doing something like this:
Otherwise path validation has the unexpected side effect of initialising the context on the router. Is that right?
Comment #29
dawehnerFair, this would cause less state changes on average.
Comment #30
alexpottLooking at this some more I'm not sure how $router->getContext() will ever not get a context given the service definition...
And maybe if for whatever reason the content is not set then re-initializing with an empty request context is better than leaving a request context that has been set from the request passed into the PathValidator.
Comment #31
Wim LeersGreat catch! Both @dawehner and I missed that. I actually made a similar mistake in the similar patch I rolled at #2293697-157: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available.
Fixed that. And fixed nits ("Contains" comment, unused "use", non-sentence comment and lack of FQCN).
Comment #32
tstoecklerAbsolutely nice catch and the new patch looks great as well.
Comment #33
alexpottHmmm... but the thing is in #30 I point out that I'm not sure that #16 is wrong because the context changing done in #31 might actually put us in a worse position :(.
Damn this one is hard to work out the impacts because of static state.
Comment #34
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedI think #30 is a valid point. I created a new/empty context object because i did not want to pollute the global state with a "random" request context just because we validated a path. It should not leak back into other parts of the system.
Comment #35
alexpottI think the fact that both #16 and #31 both passed with no test changes shows there is an untested code path. And I agree with @webflo maybe #16 it a better approach.
Comment #36
SchnWalter CreditAttribution: SchnWalter as a volunteer and commentedI've ran into an issue with xmlsitemap.module not using URL aliases when the sitemap.xml files were regenerated during cron jobs executed via the Drush CLI.
And I've managed to pin it down to the
\Drupal\Core\Path\PathValidator::getUrlIfValidWithoutAccessCheck()
and\Drupal\Core\Path\PathValidator::getPathAttributes()
throwingMethodNotAllowedException
(the method was actually empty)Applying this patch against 8.2.6 seems to fix the issue. So +1 for this patch.
(P.S.: I realized that the issue was present when using Drush v8.1.7 but not with v8.1.9, they have probably changed something and probably Drush is now providing a custom context.)
Comment #37
dawehnerI am wondering whether we should clone the route object before doing any state manipulation. This limits the risk of the additional site effect, while it doesn't remove it completely.
Comment #38
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedhttps://github.com/drush-ops/drush/pull/2512 and https://github.com/drush-ops/drush/issues/2511 are the related Drush issues.
Comment #39
Wim LeersSo, back to what #16 did.
The test here is a kernel test. All kernel tests have a request created automatically in
\Drupal\KernelTests\KernelTestBase::bootKernel()
(it pushes a request on the stack and ensures the router request context service is in sync — see\Drupal\Core\DrupalKernel::prepareLegacyRequest()
). We can only properly test what is described in #30+#33+#34 if we create a unit test. Because that would not suffer from this auto-created default request.So, here's expanded test coverage that shows what happens if you add another test case in which no request has happened yet.
Comment #40
Wim Leers#39 fails with
because
$entity->toUrl()->toString()
tries to do automatic bubbling, for which it needs the renderer, which keeps a render context per request… and no request exists yet in this case.(Yes, it's really
turtlescockroaches all the way down.)Comment #41
Wim LeersThe problem described in the IS and explicitly tested in the current patch (#16 and later) is that some CLI scripts set no request method. Notably: Drush. But #38 shows that this was fixed in Drush. The question is then: do we expect all CLI scripts to do this? If the answer is yes, then the original reason for this patch to be applied no longer exists…
And #40 shows once again why pretty much all Drupal code assumes there is a request. Without a request, most things don't work.
So… is it still worthwhile to fix this?
Well, there's still the problem I encountered in #2293697: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available: it's failing to validate the path for a route that only allows the
GET
method to be used if the validation happens during a non-GET request. So, in this case, thePathValidator
is running in a form's submit callback. Forms are submitted viaPOST
requests. SoPathValidator
is using the global request to check if the path being validated exists. But because it uses aPOST
request to do so, aMethodNotAllowedException
is thrown.IOW: this issue was created first and foremost for the
NULL
case in the test coverage. But it's also very much necessary for thePOST
case.Comment #42
Wim LeersSee #2293697-165: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available, that shows that this patch in fact does fix the fails encountered there. Again, see #2293697-157: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available for the full analysis.
Comment #43
alexpottThis might seem like a silly question. Why is the path validator checking the method?
Comment #44
Wim Leers#43: it's not. Path validator does this:
i.e. it executes full-blown route matching. And it's route matching that checks the method (among many other things), due to
\Drupal\Core\Routing\MethodFilter
.If you're wondering whether it then probably doesn't make sense to even use route matching, I agree. What we need, is for this to exist whether any of the routes match the specified path. But apparently there's not really a way to do that.
Comment #45
alexpott@Wim Leers well couldn't we return TRUE on MethodNotAllowedException? Since at this point we have validated the path and the method validation is irrelevant?
Comment #46
BerdirAlso, just in case this isn't complicated enough, if you want to see something reallly fun: #2837833-5: 'Required contexts without a value: node' when editing menu ;)
Comment #47
Wim LeersAs I said in #42, #2293697: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available is now passing with this patch applied.
#45: We could, but then where do we draw the line? Why not also return TRUE for
AccessDeniedHttpException
, because it's possible that another user is allowed to access this URL?#46: Eh… ok. Excuse me while I go and weep. Looks like #2293697 will land months, not days or weeks :(
Comment #48
alexpott@Wim Leers but access is something the PathValidator is specifically designed to test for - it is not designed to test for method applicability hence the bug.
Comment #49
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThis seems like it's a data integrity bug then, and therefore, potentially Critical. Raising it to such for triage.
Also, setting version to 8.3, because whether Critical or merely Major, I think this will warrant fixing in a patch release of 8.3, even if that's after 8.3.0 is released. It's a non-disruptive fix to a data validation bug.
To me, answering that question belongs in a separate issue. Seems like whether we want method validation or not depends on the caller. If we're validating a contact form redirect or a menu link, then what we're wanting to know is whether the path is valid for a GET request. However, if we're validating the source path on the path alias form, then potentially we don't want method validation. But regardless of how we want to handle a
MethodNotAllowedException
, so long as we're calling$router->match($path)
, we need to give the router a$request_context
appropriate for what's being validated, not what just so happens to be in the request that's invoking the validation. So, I think this issue's scope should just be what's in #40, potentially with more test coverage or other refinements to how we manage the request context, and punt any changes to how we handle aMethodNotAllowedException
to a followup.Comment #50
effulgentsia CreditAttribution: effulgentsia at Acquia commentedComment #51
effulgentsia CreditAttribution: effulgentsia at Acquia commentedComment #52
alexpott@effulgentsia so how to we not have any side-effects then? The problem is that we're affecting the global static request context that is set on the router. That just feels wrong - it's not changed - and in fact should never change.
Comment #53
dawehnerI agree with that. This issue has a clear scope so far, expanding that, just makes it harder to reason about.
I added #2852107: PathValidator::getUrlIfValid() does not support non-HTML/non-GET routes as a follow up to discuss those other bugs.
Comment #54
alexpottThanks @dawehner. Let's do #40 then and hope no one swaps out the router and if they do then they'll need to cope with things like this.
Comment #55
dawehnerGiven on my experience with the routing system I doubt that many people will override the router, and expect a different behaviour.
In an ideal world we would determine the information from the request directly on the longrun.
Comment #56
alexpottCommitted and pushed 39d1e85 to 8.4.x and 5e41fa5 to 8.3.x. Thanks!
Thanks for opening the followup @dawehner!
Comment #59
webflo CreditAttribution: webflo at UEBERBIT GmbH commentedThank you all!
Comment #60
xjmHm, I read #49 but still seems a stretch to me to call this critical. Setting back to major. Tagging for the release notes, though.
Comment #61
Wim LeersNext up: #2852107: PathValidator::getUrlIfValid() does not support non-HTML/non-GET routes.