Repository: aiogram/aiogram
Pull request #644 (2021-07-29 21:50:42)

Global filters for router
# Description

Added possibility to specify global filters for router

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

# How Has This Been Tested?

- [ ] Unittests
- [x] Manually

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes


codecov[bot]: # [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/644?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#644](https://codecov.io/gh/aiogram/aiogram/pull/644?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (86e6514) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/a70ecb767f282b030d4d73185f171a503b37092c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (a70ecb7) will **decrease** coverage by `0.08%`.
> The diff coverage is `50.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/644/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://codecov.io/gh/aiogram/aiogram/pull/644?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@             Coverage Diff             @@
##           dev-3.x     #644      +/-   ##
===========================================
- Coverage    98.64%   98.55%   -0.09%     
===========================================
  Files          268      268              
  Lines         5681     5691      +10     
===========================================
+ Hits          5604     5609       +5     
- Misses          77       82       +5     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `98.55% <50.00%> (-0.09%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/aiogram/aiogram/pull/644?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/dispatcher/event/telegram.py](https://codecov.io/gh/aiogram/aiogram/pull/644/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9kaXNwYXRjaGVyL2V2ZW50L3RlbGVncmFtLnB5) | `95.41% <50.00%> (-4.59%)` | :arrow_down: |


Patch:
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index 109bb920..ee61d1d4 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -232,20 +232,11 @@ class Dispatcher(Router):
                 "installed not latest version of aiogram framework",
                 RuntimeWarning,
             )
-            raise SkipHandler
+            raise SkipHandler()
 
         kwargs.update(event_update=update)
 
-        for router in self.chain:
-            kwargs.update(event_router=router)
-            observer = router.observers[update_type]
-            response = await observer.trigger(event, update=update, **kwargs)
-            if response is not UNHANDLED:
-                break
-        else:
-            response = UNHANDLED
-
-        return response
+        return await self.propagate_event(update_type=update_type, event=event, **kwargs)
 
     @classmethod
     async def _silent_call_request(cls, bot: Bot, result: TelegramMethod[Any]) -> None:
diff --git a/aiogram/dispatcher/event/bases.py b/aiogram/dispatcher/event/bases.py
index cb5fb2cf..8e5937ec 100644
--- a/aiogram/dispatcher/event/bases.py
+++ b/aiogram/dispatcher/event/bases.py
@@ -12,6 +12,7 @@ MiddlewareType = Union[
 ]
 
 UNHANDLED = sentinel.UNHANDLED
+REJECTED = sentinel.REJECTED
 
 
 class SkipHandler(Exception):
diff --git a/aiogram/dispatcher/event/telegram.py b/aiogram/dispatcher/event/telegram.py
index f91e8b77..ad03c06c 100644
--- a/aiogram/dispatcher/event/telegram.py
+++ b/aiogram/dispatcher/event/telegram.py
@@ -8,7 +8,7 @@ from pydantic import ValidationError
 
 from ...types import TelegramObject
 from ..filters.base import BaseFilter
-from .bases import UNHANDLED, MiddlewareType, NextMiddlewareType, SkipHandler
+from .bases import REJECTED, UNHANDLED, MiddlewareType, NextMiddlewareType, SkipHandler
 from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -32,6 +32,24 @@ class TelegramEventObserver:
         self.outer_middlewares: List[MiddlewareType] = []
         self.middlewares: List[MiddlewareType] = []
 
+        # Re-used filters check method from already implemented handler object
+        # with dummy callback which never will be used
+        self._handler = HandlerObject(callback=lambda: True, filters=[])
+
+    def filter(self, *filters: FilterType, **bound_filters: Any) -> None:
+        """
+        Register filter for all handlers of this event observer
+
+        :param filters: positional filters
+        :param bound_filters: keyword filters
+        """
+        resolved_filters = self.resolve_filters(bound_filters)
+        if self._handler.filters is None:
+            self._handler.filters = []
+        self._handler.filters.extend(
+            [FilterObject(filter_) for filter_ in chain(resolved_filters, filters)]
+        )
+
     def bind_filter(self, bound_filter: Type[BaseFilter]) -> None:
         """
         Register filter class in factory
@@ -139,6 +157,12 @@ class TelegramEventObserver:
         return await wrapped_outer(event, kwargs)
 
     async def _trigger(self, event: TelegramObject, **kwargs: Any) -> Any:
+        # Check globally defined filters before any other handler will be checked
+        result, data = await self._handler.check(event, **kwargs)
+        if not result:
+            return REJECTED
+        kwargs.update(data)
+
         for handler in self.handlers:
             result, data = await handler.check(event, **kwargs)
             if result:
diff --git a/aiogram/dispatcher/filters/command.py b/aiogram/dispatcher/filters/command.py
index ba760bef..0e46c1ec 100644
--- a/aiogram/dispatcher/filters/command.py
+++ b/aiogram/dispatcher/filters/command.py
@@ -89,7 +89,7 @@ class Command(BaseFilter):
             if isinstance(allowed_command, Pattern):  # Regexp
                 result = allowed_command.match(command.command)
                 if result:
-                    return replace(command, match=result)
+                    return replace(command, regexp_match=result)
             elif command.command == allowed_command:  # String
                 return command
         raise CommandException("Command did not match pattern")
@@ -134,7 +134,7 @@ class CommandObject:
     """Mention (if available)"""
     args: Optional[str] = field(repr=False, default=None)
     """Command argument"""
-    match: Optional[Match[str]] = field(repr=False, default=None)
+    regexp_match: Optional[Match[str]] = field(repr=False, default=None)
     """Will be presented match result if the command is presented as regexp in filter"""
 
     @property
diff --git a/aiogram/dispatcher/filters/exception.py b/aiogram/dispatcher/filters/exception.py
index f46cd739..f4a077f8 100644
--- a/aiogram/dispatcher/filters/exception.py
+++ b/aiogram/dispatcher/filters/exception.py
@@ -26,20 +26,20 @@ class ExceptionMessageFilter(BaseFilter):
     Allow to match exception by message
     """
 
-    match: Union[str, Pattern[str]]
+    pattern: Union[str, Pattern[str]]
     """Regexp pattern"""
 
     class Config:
         arbitrary_types_allowed = True
 
-    @validator("match")
+    @validator("pattern")
     def _validate_match(cls, value: Union[str, Pattern[str]]) -> Union[str, Pattern[str]]:
         if isinstance(value, str):
             return re.compile(value)
         return value
 
     async def __call__(self, exception: Exception) -> Union[bool, Dict[str, Any]]:
-        pattern = cast(Pattern[str], self.match)
+        pattern = cast(Pattern[str], self.pattern)
         result = pattern.match(str(exception))
         if not result:
             return False
diff --git a/aiogram/dispatcher/fsm/storage/redis.py b/aiogram/dispatcher/fsm/storage/redis.py
index 64c832f9..f176e209 100644
--- a/aiogram/dispatcher/fsm/storage/redis.py
+++ b/aiogram/dispatcher/fsm/storage/redis.py
@@ -45,7 +45,7 @@ class RedisStorage(BaseStorage):
         return cls(redis=redis, **kwargs)
 
     async def close(self) -> None:
-        await self.redis.close()
+        await self.redis.close()  # type: ignore
 
     def generate_key(self, bot: Bot, *parts: Any) -> str:
         prefix_parts = [self.prefix]
@@ -73,7 +73,7 @@ class RedisStorage(BaseStorage):
             await self.redis.delete(key)
         else:
             await self.redis.set(
-                key, state.state if isinstance(state, State) else state, ex=self.state_ttl
+                key, state.state if isinstance(state, State) else state, ex=self.state_ttl  # type: ignore[arg-type]
             )
 
     async def get_state(self, bot: Bot, chat_id: int, user_id: int) -> Optional[str]:
@@ -89,7 +89,7 @@ class RedisStorage(BaseStorage):
             await self.redis.delete(key)
             return
         json_data = bot.session.json_dumps(data)
-        await self.redis.set(key, json_data, ex=self.data_ttl)
+        await self.redis.set(key, json_data, ex=self.data_ttl)  # type: ignore[arg-type]
 
     async def get_data(self, bot: Bot, chat_id: int, user_id: int) -> Dict[str, Any]:
         key = self.generate_key(bot, chat_id, user_id, STATE_DATA_KEY)
diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py
index b61afc68..bc66d0de 100644
--- a/aiogram/dispatcher/router.py
+++ b/aiogram/dispatcher/router.py
@@ -3,8 +3,10 @@ from __future__ import annotations
 import warnings
 from typing import Any, Dict, Generator, List, Optional, Union
 
+from ..types import TelegramObject
 from ..utils.imports import import_module
 from ..utils.warnings import CodeHasNoEffect
+from .event.bases import REJECTED, UNHANDLED
 from .event.event import EventObserver
 from .event.telegram import TelegramEventObserver
 from .filters import BUILTIN_FILTERS
@@ -82,6 +84,22 @@ class Router:
                 for builtin_filter in BUILTIN_FILTERS.get(name, ()):
                     observer.bind_filter(builtin_filter)
 
+    async def propagate_event(self, update_type: str, event: TelegramObject, **kwargs: Any) -> Any:
+        kwargs.update(event_router=self)
+        observer = self.observers[update_type]
+        response = await observer.trigger(event, **kwargs)
+        if response is REJECTED:
+            return UNHANDLED
+        if response is not UNHANDLED:
+            return response
+
+        for router in self.sub_routers:
+            response = await router.propagate_event(update_type=update_type, event=event, **kwargs)
+            if response is not UNHANDLED:
+                break
+
+        return response
+
     @property
     def chain_head(self) -> Generator[Router, None, None]:
         router: Optional[Router] = self
diff --git a/poetry.lock b/poetry.lock
index 4e6bcb7d..7f324bfc 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -40,7 +40,7 @@ python-socks = {version = ">=1.0.1", extras = ["asyncio"]}
 
 [[package]]
 name = "aioredis"
-version = "2.0.0a1"
+version = "2.0.0"
 description = "asyncio (PEP 3156) Redis support"
 category = "main"
 optional = false
@@ -296,6 +296,14 @@ importlib-metadata = "*"
 jinja2 = ">=2.9.0"
 pygments = ">=2.2.0"
 
+[[package]]
+name = "frozenlist"
+version = "1.1.1"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
 [[package]]
 name = "furo"
 version = "2021.6.18b36"
@@ -1201,7 +1209,7 @@ redis = ["aioredis"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "ef3571030ff35c2a05e01dca86e9347239e98ad0f45bed6f5d9a73121013f376"
+content-hash = "c51e22cdb0e17fb996fda81c5484d34f3dff0e57511380b1103a1d53c9416440"
 
 [metadata.files]
 aiofiles = [
@@ -1252,8 +1260,8 @@ aiohttp-socks = [
     {file = "aiohttp_socks-0.5.5.tar.gz", hash = "sha256:2eb2059756bde34c55bb429541cbf2eba3fd53e36ac80875b461221e2858b04a"},
 ]
 aioredis = [
-    {file = "aioredis-2.0.0a1-py3-none-any.whl", hash = "sha256:32d7910724282a475c91b8b34403867069a4f07bf0c5ad5fe66cd797322f9a0d"},
-    {file = "aioredis-2.0.0a1.tar.gz", hash = "sha256:5884f384b8ecb143bb73320a96e7c464fd38e117950a7d48340a35db8e35e7d2"},
+    {file = "aioredis-2.0.0-py3-none-any.whl", hash = "sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb"},
+    {file = "aioredis-2.0.0.tar.gz", hash = "sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db"},
 ]
 alabaster = [
     {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
@@ -1401,6 +1409,49 @@ flake8-html = [
     {file = "flake8-html-0.4.1.tar.gz", hash = "sha256:2fb436cbfe1e109275bc8fb7fdd0cb00e67b3b48cfeb397309b6b2c61eeb4cb4"},
     {file = "flake8_html-0.4.1-py2.py3-none-any.whl", hash = "sha256:17324eb947e7006807e4184ee26953e67baf421b3cf9e646a38bfec34eec5a94"},
 ]
+frozenlist = [
+    {file = "frozenlist-1.1.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:968b520ef969541b2c8f47d9a13c78e080806dc97862434d29163d44c2c1d709"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:451b445120ea95d86af3817bbd4d67ab77269fe7f055dc67b8c70bf4633f4efe"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:313384e54a7285a6f20ca6530b207a0a9cf6ebcda6c7b074ee802e4a82a0a6dc"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:3c4f7399e7338a5788d32802017f94aaab3267afa8b1a663272b81eee7193e66"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1e7b18fdf6682028f512d3e6142b79ca95b9b66f30c1bec2be237160d9eb6518"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:7622f5c4c3dfaa09b9c6a62fb1af94da124626bb30f3ad9095f9cec6328074c1"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:657341b9bc166d3f7418d37e1decf6d95485501e0d0e7da1a26a881e624216c6"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:fc6d994de78b11e1f465f2224c56858eb52cb51c8f9faf0c33e5799184d414a7"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:08428f9d0178b6fa0da95a42ab87a5b20ed2a707bacc97e3689e96ae6cab13fa"},
+    {file = "frozenlist-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5c4c42bdbf5754010e0cc5cc0f91019437839bc6b7e585262bcc126557a244bc"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:32491ac26e72e5f35913887bc3ab7bcfe562b4fb65b0e58350fce6efa22fec75"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:d160a73e4a034a857a98384b5e05204c375489d2bbb6ecf1ee8fc124735028fa"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b3cf6737afc4347092a0c8392b4c0e77acc5594e73f4aef355705117a945743a"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:efb805e383836250bef3c99f1857c432a8941c802d0ed7767751315617a54794"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:2f1f56a36962e28c304872797e226cd646395381de97517870fb819ff7b4f496"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:7443d815fd9ff2de75b810e192cfa92854bada43aed47ec1598766c7bc9d4a40"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe463e7b3cd089d221f33bd9c22cfad2726622b2a96c3af56a8eb5a71c0943bd"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:c228886891dc0170d21acbfb62fea801856c3fa207619c973e17d96455ab83e3"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:de170ea97e7b5051a13989ea457300b8159c00455d2207d22afb6b129a433152"},
+    {file = "frozenlist-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:158c6258ab4ee8a01470d86e75a7514091391b27bb400ba28a7f6a30466cc8e0"},
+    {file = "frozenlist-1.1.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:572a5a0977b1bd2f15183a352df907726b20da5f91cd1242343b0d72ac677be6"},
+    {file = "frozenlist-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:748150da8bbd9cbe1b29f0965a675b5732337ae874eed47ccb48dfa75815d0d7"},
+    {file = "frozenlist-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:caba5ae97c40020771502866dee5024b0031187293185ca5c7714ea52a824a92"},
+    {file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:aea1b84bbebec7c46cd59da13aff90e23bece13bba91974a305bd555f66a72f3"},
+    {file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:d07f08268b8d37c357f4d34272f1f7588a0618d3fa509a87ad614b5e1cf7109f"},
+    {file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:66f7ba888fe51685502be51ad548b226eb4214fcab0ef48672a2a91a4de08417"},
+    {file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:4282d897ea190b5e38a18fc3b70295e20e00af7734892250876e1e1b452a7dc8"},
+    {file = "frozenlist-1.1.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:84ef9f6f6f8e2dd9cf828367c61715202a781ef6d32caa9a016d9055a7daef8b"},
+    {file = "frozenlist-1.1.1-cp38-cp38-win32.whl", hash = "sha256:d5cba2a537bcf8b4abffc9e01b037eb4ca5c9d1cb29d575fd433f82919a04c68"},
+    {file = "frozenlist-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:6c1cefdc3666507f7241b120b828e223c1dfb18e599f960ebbd0558de5010efe"},
+    {file = "frozenlist-1.1.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a6d2d80222eefe6e08b8167005e5a0c1a05ce784ce97de4d6d693be7e2a99862"},
+    {file = "frozenlist-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8fbdc86968f71d1d1e216f1f3467da96571b092378ad55b7eb6fc9f3ff877902"},
+    {file = "frozenlist-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1032a7eb76ca47cb94dcfd05a289dfb2f31b5e155c9cd845f97a56526eca9800"},
+    {file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4fdfb300d205f3d007462d66c9e8ffa89d7b1b3699e538ae7344845223291ff0"},
+    {file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:e2d35804cf42b58e42e9b2cca6a2a5bb7155bb545808ff652503a8bacab2be5d"},
+    {file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:a2ebc6dd4f73f39212073add6b3a629a4274ed0a5e43c2fa87bd91957f511450"},
+    {file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:9ee0bca9801eea5431680bdf22817b1b07310474ac284a3aa7a3902d0dba2382"},
+    {file = "frozenlist-1.1.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:08a4f1bd182659416c8ae518ef8a63c37953eb2d4bd77cf8b45941a90e87d27c"},
+    {file = "frozenlist-1.1.1-cp39-cp39-win32.whl", hash = "sha256:803bc0fdb904a762b0a49572fe2f1cb2a03ade5514b265971da5c3e7a8b14798"},
+    {file = "frozenlist-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:301220a5752fc2585ef97794b1dc88f87d77f5c1951782488896fcb6a732f883"},
+    {file = "frozenlist-1.1.1.tar.gz", hash = "sha256:32fa8c86eee5c6f11b44863c0d3e945b2b1a03df3bf218617e832f1f04ba3146"},
+]
 furo = [
     {file = "furo-2021.6.18b36-py3-none-any.whl", hash = "sha256:a4c00634afeb5896a34d141a5dffb62f20c5eca7831b78269823a8cd8b09a5e4"},
     {file = "furo-2021.6.18b36.tar.gz", hash = "sha256:46a30bc597a9067088d39d730e7d9bf6c1a1d71967e4af062f796769f66b3bdb"},
diff --git a/pyproject.toml b/pyproject.toml
index b9e92418..d9c29089 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,8 +37,9 @@ pydantic = "^1.8.1"
 Babel = "^2.9.1"
 aiofiles = "^0.6.0"
 async_lru = "^1.0.2"
+frozenlist = "^1.1.1"
 aiohttp-socks = { version = "^0.5.5", optional = true }
-aioredis = { version = "^2.0.0a1", allow-prereleases = true, optional = true }
+aioredis = { version = "^2.0.0", allow-prereleases = true, optional = true }
 magic-filter = { version = "1.0.0a1", allow-prereleases = true }
 sphinx = { version = "^3.1.0", optional = true }
 sphinx-intl = { version = "^2.0.1", optional = true }


Test patch:
diff --git a/tests/test_api/test_methods/test_delete_my_commands.py b/tests/test_api/test_methods/test_delete_my_commands.py
index a39d080b..b5546b25 100644
--- a/tests/test_api/test_methods/test_delete_my_commands.py
+++ b/tests/test_api/test_methods/test_delete_my_commands.py
@@ -1,6 +1,6 @@
 import pytest
 
-from aiogram.methods import BanChatMember, DeleteMyCommands, Request
+from aiogram.methods import DeleteMyCommands, Request
 from tests.mocked_bot import MockedBot
 
 
diff --git a/tests/test_dispatcher/test_event/test_telegram.py b/tests/test_dispatcher/test_event/test_telegram.py
index d4306b1a..13f70503 100644
--- a/tests/test_dispatcher/test_event/test_telegram.py
+++ b/tests/test_dispatcher/test_event/test_telegram.py
@@ -4,7 +4,7 @@ from typing import Any, Awaitable, Callable, Dict, NoReturn, Union
 
 import pytest
 
-from aiogram.dispatcher.event.bases import SkipHandler
+from aiogram.dispatcher.event.bases import REJECTED, SkipHandler
 from aiogram.dispatcher.event.handler import HandlerObject
 from aiogram.dispatcher.event.telegram import TelegramEventObserver
 from aiogram.dispatcher.filters.base import BaseFilter
@@ -233,3 +233,48 @@ class TestTelegramEventObserver:
         assert my_middleware3 in middlewares
 
         assert middlewares == [my_middleware1, my_middleware2, my_middleware3]
+
+    def test_register_global_filters(self):
+        router = Router(use_builtin_filters=False)
+        assert isinstance(router.message._handler.filters, list)
+        assert not router.message._handler.filters
+
+        my_filter = MyFilter1(test="pass")
+        router.message.filter(my_filter)
+
+        assert len(router.message._handler.filters) == 1
+        assert router.message._handler.filters[0].callback is my_filter
+
+        router.message._handler.filters = None
+        router.message.filter(my_filter)
+        assert len(router.message._handler.filters) == 1
+        assert router.message._handler.filters[0].callback is my_filter
+
+    @pytest.mark.asyncio
+    async def test_global_filter(self):
+        r1 = Router()
+        r2 = Router()
+
+        async def handler(evt):
+            return evt
+
+        r1.message.filter(lambda evt: False)
+        r1.message.register(handler)
+        r2.message.register(handler)
+
+        assert await r1.message.trigger(None) is REJECTED
+        assert await r2.message.trigger(None) is None
+
+    @pytest.mark.asyncio
+    async def test_global_filter_in_nested_router(self):
+        r1 = Router()
+        r2 = Router()
+
+        async def handler(evt):
+            return evt
+
+        r1.include_router(r2)
+        r1.message.filter(lambda evt: False)
+        r2.message.register(handler)
+
+        assert await r1.message.trigger(None) is REJECTED
diff --git a/tests/test_dispatcher/test_filters/test_exception.py b/tests/test_dispatcher/test_filters/test_exception.py
index 4dd6d5d9..23d850da 100644
--- a/tests/test_dispatcher/test_filters/test_exception.py
+++ b/tests/test_dispatcher/test_filters/test_exception.py
@@ -8,12 +8,12 @@ from aiogram.dispatcher.filters import ExceptionMessageFilter, ExceptionTypeFilt
 class TestExceptionMessageFilter:
     @pytest.mark.parametrize("value", ["value", re.compile("value")])
     def test_converter(self, value):
-        obj = ExceptionMessageFilter(match=value)
-        assert isinstance(obj.match, re.Pattern)
+        obj = ExceptionMessageFilter(pattern=value)
+        assert isinstance(obj.pattern, re.Pattern)
 
     @pytest.mark.asyncio
     async def test_match(self):
-        obj = ExceptionMessageFilter(match="KABOOM")
+        obj = ExceptionMessageFilter(pattern="KABOOM")
 
         result = await obj(Exception())
         assert not result
diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py
index c84239b1..1516c33c 100644
--- a/tests/test_dispatcher/test_router.py
+++ b/tests/test_dispatcher/test_router.py
@@ -1,6 +1,6 @@
 import pytest
 
-from aiogram.dispatcher.event.bases import SkipHandler, skip
+from aiogram.dispatcher.event.bases import SkipHandler, skip, UNHANDLED
 from aiogram.dispatcher.router import Router
 from aiogram.utils.warnings import CodeHasNoEffect
 
@@ -122,3 +122,17 @@ class TestRouter:
             skip()
         with pytest.raises(SkipHandler, match="KABOOM"):
             skip("KABOOM")
+
+    @pytest.mark.asyncio
+    async def test_global_filter_in_nested_router(self):
+        r1 = Router()
+        r2 = Router()
+
+        async def handler(evt):
+            return evt
+
+        r1.include_router(r2)
+        r1.message.filter(lambda evt: False)
+        r2.message.register(handler)
+
+        assert await r1.propagate_event(update_type="message", event=None) is UNHANDLED