Problem/Motivation
The block visibility system in Drupal 7 allowed a block to be shown by default on every page, but then hidden unless a specific condition was met (i.e. node type).
This was ported to the context system in Drupal 8, which does not have a concept of 'optional' contexts. The result is that the only way to make a context optional is to wrap context mapping in a try/catch.
There are two issues with this:
- currently a missing context means that cacheability metadata is unreliable, #2521956: Missing contexts prevent caching of block access is open for that.
- additionally, since there is clearly a use case for optional contexts, we should cater for that in the context API, rather than abusing exceptions.
Proposed resolution
Add an additional context handler interface which handles the missing context interface and returns FALSE rather than throwing an exception.
Remaining tasks
User interface changes
API changes
Data model changes
When you add a ctools user block, it is unable to get the context and throws an error.

Drupal\Component\Plugin\Exception\ContextException: Required contexts without a value: entity. in Drupal\Core\Plugin\Context\ContextHandler->applyContextMapping() (line 117 of /Users/japerry/Sites/d8/drupal/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php).
| Comment | File | Size | Author |
|---|---|---|---|
| #14 | drupal8-block-missing-context-2620126-14.patch | 5.06 KB | dsnopek |
| #6 | 2620126-block-6.patch | 1.08 KB | tim.plunkett |
| Block_layout___d8_and_ContextHandler_php_-_Drupal8_-____PhpstormProjects_Drupal8_.png | 170.64 KB | japerry |
Comments
Comment #2
tim.plunkettThis seems like a core bug in \Drupal\node\ContextProvider\NodeRouteContext::getAvailableContexts().
Why do we blindly state we'll have a context available, and not restrict it in some way to only show on node pages?
Comment #3
japerryUpping to Critical since its pretty easy to trip over this WSOD bug
Comment #4
dsnopekI agree that this is a core bug. Rather than throwing an exception, it should just not show the block.
Comment #5
japerryRe-titling and moving to the core issue queue. This is pretty big because any block that requires a context will throw the exception when being displayed on a page that cannot fulfill the requirement. Instead, it probably should just not display.
Comment #6
tim.plunkettIt would have been easy to write a unit test for this, but someone refactored this code into a protected static method.
Comment #7
berdirFor the record, page manager has the same problem, pretty sure we need to fix it there too. Unless you did recently but last time I checked, it still exploded nicely :)
I also know that we are filtering, but we know that those two contexts (node and current user), *might* be available.
Might also be a caching problem, do we need to worry about that?
Comment #8
catchThis is catching the exception, but shouldn't we not throw that in the first place?
Comment #9
dsnopekHm. We need some indication from the API that the context mapping failed. It could be a boolean return value, but that's an API change. This patch seems like the best we can do without changing the API.
Comment #10
berdirYeah, I find that very weird too. But yes, that would be an API change. See also #2521956: Missing contexts prevent caching of block access and #2375695-42: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed (comment #42 in case the link doesn't work).
Quoting myself from there:
Comment #11
catchWhile we use exceptions for code flow in 404/403 pages because Symfony, and form API because we couldn't think of anything else, would really like to avoid any more cases. Exceptions should really be for actual errors and not just eaten - if this is valid behaviour, the code should not throw an exception, if it isn't, we shouldn't hit the condition in the first place.
Can add a new method if we have to, then it wouldn't be an API change.
Comment #12
eclipsegc commentedTim's patch is functionally replicating what we already do for conditions.
Eclipse
Comment #13
catchLooks like that was added in #2339151: Conditions / context system does not allow for multiple configurable contexts, eg. language types but I don't see it discussed in that issue.
Comment #14
dsnopekHere's a patch that takes @catch's suggestion in #11 of adding a new method. However, after attempting this exercise, I'd personally prefer Tim's patch in #6.
While it's possible to add a new method with out breaking BC, it's complicated by the fact that this is a service ('context.handler') that has an interface (
ContextHandlerInterface). We can't simply add the new method to the existing interface, because then some contrib module which replaced that service with it's own class implementing that interface would cause a fatal error. So, we have to add a new interface (I called itContextHandlerInterface2for lack of a better name - please just ignore that the name is terrible).And we can't require that any class that replaces the 'context.handler' service implements the new interface, because that would also be a BC break. So, code that uses that service needs to check if it implements the new interface before calling the new method (which I called
applyContextMapping2()- again ignore the terrible name). This also means that we need to have a fallback that uses only methods from the original interface.So, the net result is:
ContextHandlerInterface2but I have no idea how to name the method...While it'd be better not to use an exception here, I think those cons out-weigh the benefit.
So, like I said at the beginning, I think we should just go with #6 (which still needs tests).
Comment #15
neclimdulReally we don't need the instance check we just wrap them both in try catch and one just won't trigger it.
Which sort of gets to the heart of by hesitance at this approach. Since we have to support both this interface doesn't really buy us anything. I think we should go ahead with tim's patch mostly as is (should document what's going on and why we basically throw the exception away).
I'd also like to follow up with improving the context interface to allow reporting of missing contexts(this could be a separate interface allowing us to compose them and provide BC). This would allow us to skip throwing exceptions when its implemented and when the interface is implemented we can do more sane logic like this instead of relying on exceptions.
or in an admin interface:
Comment #16
neclimdulReally we don't need the instance check we just wrap them both in try catch and one just won't trigger it.
Which sort of gets to the heart of by hesitance at this approach. Since we have to support both this interface doesn't really buy us anything. I think we should go ahead with tim's patch mostly as is (should document what's going on and why we basically throw the exception away).
I'd also like to follow up with improving the context interface to allow reporting of missing contexts(this could be a separate interface allowing us to compose them and provide BC). This would allow us to skip throwing exceptions when its implemented and when the interface is implemented we can do more sane logic like this instead of relying on exceptions.
or in an admin interface:
Comment #17
dsnopekIt's a different method, though, because we can't change the signature on the original method. This might have been hard to catch because the method names are so similar (
applyContextMapping()vsapplyContextMapping2())?In any case, I completely agree with your conclusion. :-)
Comment #18
phenaproximaComment #19
dsnopekThe patch that @phenaproxima references above will also fix this issue (confirmed while doing testing on a CTools patch for a block that takes context).
Comment #20
berdirLets close this as a duplicate then.
Comment #21
catchIt gets us two things:
1. We're not creating then eating the exception as part of the code flow, which on 99.9% of sites would be the case since the context handler probably won't be different from the core one.
2. When we get to 9.x we can either drop the old interface or rename the new one back over the old interface. Either way modules would either have to do nothing to port, or maybe deal with a rename, and then the if/else just becomes dead code then.
Also it might be possible to handle the instanceof check with a new service definition using the new interface, not sure about that though - it's not something we've really dealt with yet.
I've committed #2635238: Contexts not mapped in time to use them in BlockInterface::access() but I still think we should fix this at the API level so that eventually calling code doesn't have to jump through hoops. So re-opening for the remaining discussion here.
Comment #22
catchComment #23
catchComment #24
dsnopekComment #25
eclipsegc commentedSo, I'm not sure of the importance of this issue any longer. Looking at the issue summary it's demonstrably false since the ContextDefinition system leveraged by plugins absolutely does have handling for optional contexts, they just simply must be defined as such in the plugin definition. The \Drupal\condition_test\Tests\OptionalContextConditionTest actually has test coverage for this use case and the \Drupal\condition_test\Plugin\Condition\OptionalContextCondition condition plugin shows how this is defined. A block plugin context will work the same way since they're all built on the same API. In short, optional contexts are absolutely supported by the plugin system and block specifically. For the sake of saving other looking it up:
The "required = FALSE" portion of the ContextDefinition annotation will set a plugin's contexts to optional and it will no longer throw an exception on missing contexts.
Eclipse
Comment #26
catchWhy aren't we using that for the NodeRouteContext then?
Comment #27
dsnopek@catch: I'm not sure I entirely understand how the issue you're describing is different than #2521956: Missing contexts prevent caching of block access?
As @EclipseGC points out, there is the idea of optional contexts built into the context system. What's missing is a way to distinguish a mapping failure between (a) a missing context (ie. no context object was passed in at all) and (b) an empty context (a context object was passed in, but doesn't have a value and is required). This is what #2521956 is attempting to address.
So, if the bug described here is the same as #2521956, we should work on that there, and maybe make this issue just be about switching from an exception to a return value (and the backcompat stuff necessary to make that happen)?
Comment #28
eclipsegc commentedNodeRouteContext extracts the node object from the route upcasting and makes it available in the array of runtime contexts to blocks and their visibility conditions. If a block (or condition) sets a mapping that states it should use the node context provided by NodeRouteContext, then it an attempt will be made to map it from the contexts array to the appropriate plugin(s). However, if those plugins state that they REQUIRE a node context (instead of saying "oh I can figure it out without a node, but I'll use one if you have one") then they cannot operate if the context is missing and will throw an exception if the context is not currently available (i.e. you're not on a node/{node} page).
So, case in point, the node_type condition cannot determine the bundle of the node you're currently visiting if no node is available. Therefore, if it is executed without a node, it will throw an exception. Plugins are built to be aware of what contexts are available during configuration time, but core is not smart enough to know this, so it's really a failure of the block UI that we have to catch these exceptions since they are not intended to be part of typical runtime. Since core has no notion of pages or what's available on them, core resorts to interpreting these exceptions as "access false". As core becomes more "page aware", we will be able to predict what plugins can be executed on a given page and only allow for those plugins to be configured. This is true of blocks, conditions and any other plugin we might introduce into the page-rendering process.
Also, conditions return boolean values, not access checks. This is because conditions are not intended for exclusive use in access, i.e. rules makes use of them too and so can many other systems (pathauto can also use them in the contrib work we're doing). To this end they would not simply return some default if there's missing required contexts as that does not properly represent the evaluation they claim to do.
I hope that explains what's going on here.
Eclipse
Comment #29
berdirThe problem is that if you configure a node type visibility condition, then it is *not* optional. It's not the provider that decides if a context or optional or not, it's the plugin that requires it. That a provided context can even define that is weird and before #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed, we even checked the provided context for required/optional instead of the plugin that receives it.
NodeRouteContext doesn't always give you a context value but it does always give you a context with cacheabliity metadata, so it is doing it correctly.
Comment #30
fagoI ran into a quite related issue with Rules (again). The problem is that there is no way to check for a set context value without having to use try/catch while doing so. While this matches exactly the issue title, it's not necessarily directly related as the exception is thrown at a different place: the Context class. Thus I opened #2677162: Context::getContextValue() violates ContextInterface by throwing an unexpected exception for it.
Comment #31
fagoRight. The 'required' value of available context can tell you though whether it could be unset.
Comment #32
jibranI faced the similar issue using panels #2679124: Missing contexts throw an exception.
Comment #34
japerrySo this issue is pretty different from the original summary. As for the original bug I reported, it was fixed with #2635238: Contexts not mapped in time to use them in BlockInterface::access().
Therefore, I'd recommend we either:
1) Update the summary to reflect the concerns catch has in comment 21
2) Close this issue and continue the conversation in #2677162: Context::getContextValue() violates ContextInterface by throwing an unexpected exception
Comment #35
eclipsegc commentedOk, as outlined in the issue summary of #2677162: Context::getContextValue() violates ContextInterface by throwing an unexpected exception you absolutely can determine if a context has a value. As outlined in this issue the plugin is responsible for setting whether a context is required or optional, and there is support for optional contexts which will not throw an exception. Though berdir points out we used to check for the required-ness of contexts defined by providers, that was simply wrong and probably my fault for forgetting which context object/definition I was dealing with at the time.
Best as I can tell, this all functions as expected after a handful of fixes that have landed in 8.0.x and I don't see any reason this issue should remain open.
Eclipse