aio-libs/aiohttp · error · ValueError

Bad pattern

Error message

Bad pattern '{pattern}': {exc}

What it means

After assembling a regex pattern from the path template, aiohttp attempts re.compile(). If the user-supplied regex inside a {var:regex} variable is invalid Python regex, re.error is raised and aiohttp wraps it as ValueError with the offending pattern and the underlying error. This surfaces regex syntax errors in dynamic route definitions at registration time rather than at match time.

Solutions

  1. Test the inner regex in isolation with re.compile before putting it in the route: e.g. re.compile(r'\d+') should succeed.
  2. Fix the regex syntax (balance parentheses, use valid escapes).
  3. Simplify the regex or validate in the handler instead of the route template.

Example fix

// before
app.router.add_get('/users/{id:\d+(}', handler)  # unbalanced group
// after
app.router.add_get('/users/{id:\d+}', handler)   # valid regex
Defensive patterns

Strategy: validation

Validate before calling

import re

def validate_route_regex(var_regex: str) -> None:
    try:
        re.compile(var_regex)
    except re.error as e:
        raise ValueError(f'Invalid route regex {var_regex!r}: {e}') from e

Prevention

When it happens

Trigger: Registering a route like app.router.add_get('/users/{id:\d+(}', handler) where the custom regex '\d+(' is malformed (unbalanced group). Any invalid regex construct — stray quantifiers, unbalanced parentheses, invalid escapes — inside a {name:regex} segment triggers this.

Common situations: Writing a custom regex for path validation with a syntax error; copy-pasting regex from a different regex flavor that Python rejects; overly clever regex that fails to compile; forgetting that the braces themselves are delimiters and the inner pattern must be valid on its own.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/7d1982d97ba51800. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/web_urldispatcher.py:435

                continue

            match = self.DYN_WITH_RE.fullmatch(part)
            if match:
                pattern += "(?P<{var}>{re})".format(**match.groupdict())
                formatter += "{" + match.group("var") + "}"
                continue

            if "{" in part or "}" in part:
                raise ValueError(f"Invalid path '{path}'['{part}']")

            part = _requote_path(part)
            formatter += part
            pattern += re.escape(part)

        try:
            compiled = re.compile(pattern)
        except re.error as exc:
            raise ValueError(f"Bad pattern '{pattern}': {exc}") from None
        assert compiled.pattern.startswith(PATH_SEP)
        assert formatter.startswith("/")
        self._pattern = compiled
        self._formatter = formatter

    @property
    def canonical(self) -> str:
        return self._formatter

    def add_prefix(self, prefix: str) -> None:
        assert prefix.startswith("/")
        assert not prefix.endswith("/")
        assert len(prefix) > 1
        self._pattern = re.compile(re.escape(prefix) + self._pattern.pattern)
        self._formatter = prefix + self._formatter

    def _match(self, path: str) -> dict[str, str] | None:
        match = self._pattern.fullmatch(path)

View on GitHub (pinned to d041d4d0fd)