aio-libs/aiohttp · error · ValueError

Gunicorn's style options in form of `%(name)s` are not suppo

Error message

Gunicorn's style options in form of `%(name)s` are not supported for the log formatting. Please use aiohttp's format specification to configure access log formatting: http://docs.aiohttp.org/en/stable/logging.html#format-specification

What it means

Raised by GunicornWebWorker._get_valid_log_format() as a ValueError when the access_log_format string contains Gunicorn-style named placeholders like `%(name)s`. aiohttp's AccessLogger uses its own format specification (positional `%s` and `{name}` style), not Gunicorn's `%(name)s` dictionary interpolation, so such formats are rejected at worker startup.

Source

Thrown at aiohttp/worker.py:238

        See ssl.SSLSocket.__init__ for more details.
        """
        if ssl is None:  # pragma: no cover
            raise RuntimeError("SSL is not supported.")

        ctx = ssl.SSLContext(cfg.ssl_version)
        ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
        ctx.verify_mode = cfg.cert_reqs
        if cfg.ca_certs:
            ctx.load_verify_locations(cfg.ca_certs)
        if cfg.ciphers:
            ctx.set_ciphers(cfg.ciphers)
        return ctx

    def _get_valid_log_format(self, source_format: str) -> str:
        if source_format == self.DEFAULT_GUNICORN_LOG_FORMAT:
            return self.DEFAULT_AIOHTTP_LOG_FORMAT
        elif re.search(r"%\([^\)]+\)", source_format):
            raise ValueError(
                "Gunicorn's style options in form of `%(name)s` are not "
                "supported for the log formatting. Please use aiohttp's "
                "format specification to configure access log formatting: "
                "http://docs.aiohttp.org/en/stable/logging.html"
                "#format-specification"
            )
        else:
            return source_format


class GunicornUVLoopWebWorker(GunicornWebWorker):
    def init_process(self) -> None:
        import uvloop

        asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

        super().init_process()

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Use aiohttp's format spec: positional `%s` for fixed fields and `{header_name}` / `{client_ip}` etc. See the logging docs link in the error.
  2. If you want gunicorn's default behavior, leave access_log_format unset so it matches DEFAULT_GUNICORN_LOG_FORMAT and is auto-converted to aiohttp's DEFAULT_AIOHTTP_LOG_FORMAT.
  3. Convert any `%(h)s` style tokens to the equivalent aiohttp `{client_ip}` / `%s` fields.

Example fix

# before (gunicorn.cfg)
access_log_format = '%(h)s %(l)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'

# after (aiohttp format spec)
access_log_format = '%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"'
Defensive patterns

Strategy: validation

Validate before calling

import re

def validate_log_format(fmt: str) -> str:
    if re.search(r"%\([^\)]+\)", fmt):
        raise ValueError("use aiohttp format spec, not gunicorn %(name)s")
    return fmt

Type guard

import re

def is_aiohttp_log_format(fmt: str) -> bool:
    return not re.search(r"%\([^\)]+\)", fmt)

Prevention

When it happens

Trigger: Setting gunicorn's `access_log_format` to a string containing a `%(...)s` pattern (e.g. gunicorn's default `%(h)s %(l)s ...`) while using the aiohttp GunicornWebWorker. The regex search at aiohttp/worker.py:237 detects the pattern and raises.

Common situations: Copying a Gunicorn access log format from another project into the aiohttp worker config; relying on gunicorn's default format which the worker remaps automatically only when it exactly equals DEFAULT_GUNICORN_LOG_FORMAT; mixing gunicorn and aiohttp logging docs.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/98132f6e50dd6201.json. Report an issue: GitHub.