Skip to content

Conversation

@tkosman
Copy link

@tkosman tkosman commented Jul 30, 2025

🔥 Real-Life Horror Story: Config Edition

We have an app that accepts hexadecimal prefixes (0–f) as a configuration parameter. To support environment-based configuration, we used Sanic’s built-in environment variable parsing by setting SANIC_PREFIX with values from 0 to f.

You can imagine my surprise when I saw this in one of 16 pods running our app:

2025-07-30 08:14:23,108 [INFO] Beginning update for prefix: False

After digging into the documentation, I discovered that Sanic automatically interprets 'f' as bool type False.
Even more surprising was that Sanic's config parsing does not respect the types defined in the defaults dictionary. Also, custom converters only receive the value, without access to the associated key or default—making proper type handling impossible.

To solve this, I implemented a custom DetailedConverter that provides access to the full key, the config key, the raw value, and the defaults. This allows us to:

  • Cast the value to the type defined in DEFAULTS.
  • Raise meaningful errors when a mismatch occurs.

This enforces strong typing based on your default config and avoids silent issues like the "f" → False coercion.

Below solution for my issue:

import os
import sanic
from sanic.config import Config
from sanic.config import DetailedConverter
from sanic.log import logger

DEFAULTS = {
    "CONFIG_STRING": str("abc"),
    "CONFIG_INT": int(123),
    "CONFIG_BOOL": bool(True),
}

class DefaultsTypeCastingConverter(DetailedConverter):
    def __call__(self, full_key: str, config_key: str, value: str, defaults: dict):
        try:
            if config_key in defaults:
                return type(defaults[config_key])(value)
        except (ValueError, TypeError) as e:
            raise TypeError(f"Configuration environment variable '{full_key}' type mismatch: expected"
                            f" {type(defaults[config_key]).__name__}, got {type(value).__name__}") from e

# Simulate environment variables for testing
os.environ["SANIC_CONFIG_STRING"] = "overridden_string"
os.environ["SANIC_CONFIG_INT"] = "456"
os.environ["SANIC_CONFIG_BOOL"] = "False"

# This would rise an error if uncommented, as it conflicts with its type in DEFAULTS
#os.environ["SANIC_CONFIG_INT"] = "i_am_definitely_not_an_int"

APP = sanic.Sanic("TestApp", config=sanic.Config(defaults=DEFAULTS, converters=[DefaultsTypeCastingConverter()]))
@APP.route("/")
async def a(request):
    return

# Print defaults (overridden) with their types
logger.warning("Sanic app initialized with overridden defaults:")
logger.warning("-" * 40)
for key, value in APP.config.items():
    if key in DEFAULTS:
        logger.warning(f"{key}: {value} (type: {type(value).__name__})")

if __name__ == "__main__":
    APP.run(host="127.0.0.1", port=8000, single_process=True)

@tkosman tkosman requested review from a team as code owners July 30, 2025 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant