Illogical behaviour of python web framework (issue investigation story)
Tags: python, asgi, webframework, issues
Published: 2023-02-20 19:17:38

TLDR: _Strange issues with Sanic framework on my new pet project were fixed in ugly way and I do not like it but do not see the other way and asking for help/advice._

----

Recently I have tried a new python ASGI web framework for my new pet project and faced an weirdest issue on my memory.

Our hero framework here is [Sanic](https://sanic.dev/en/) - new, modern async web framework with great slogan "Build fast. Run fast." on the site.

Let's move to the story itself.

I have created an web application backbone with couple endpoints and it works at the beginning.

code like this:
```python
from sanic import Sanic


app = Sanic("pet project N7355")


@app.route("/")
async def index_handler(request):
    return json(dict(name="pet project N7355", version="0.1.0"))


@app.route("/system/health")
async def health_handler(request):
    return json(dict(status="ok"))


@app.route("/system/stat")
async def stat_handler(request):
    return json(dict())


app.run(host='0.0.0.0', port=8765, debug=debug)
```

But after I split that code to modules and moved initialisation of endpoints to separate function I have started to see an error about Sanic cannot start as there is no routes defined.

new code:
main.py
```python
from sanic import Sanic
from routes import init_routes


app = Sanic("pet project N7355")
init_routes(app)

app.run(host='0.0.0.0', port=8765, debug=debug)
```

routes.py
```python

def init_routes(app):
    @app.route("/")
    async def index_handler(request):
        return json(dict(name="pet project N7355", version="0.1.0"))


    @app.route("/system/health")
    async def health_handler(request):
        return json(dict(status="ok"))


    @app.route("/system/stat")
    async def stat_handler(request):
        return json(dict())
```

Similar approach for defining a routes was used in my previous project with Flask and worked perfectly fine but not now.
I have tried to run application in single thread mode as suggested in error message but there was no such error in this more.

I thought what a strange issue and fixed that by workaround (dirty hack in fact) - moved "/" method from function in module to main file.

My next step was about connecting to database.

I have used [PeeWee](https://peewee.readthedocs.io) ORM (not an mainstream choice but it looks simple and easy) and as there is no plugins to link Sanic with PeeWee I used approach listed on Sanic site - put a connection to application context ``app.ctx.db = db`` and faced another error - "cannot use not initialised database".

code with db connection:
main.py
```python
from sanic import Sanic
from routes import init_routes
from db import init_db


app = Sanic("pet project N7355")
app.ctx.db = init_db()

# workaround to fix routes issue in multithread mode
@app.route("/")
async def index_handler(request):
    return json(dict(name="pet project N7355", version="0.1.0"))


init_routes(app)

app.run(host='0.0.0.0', port=8765, debug=debug)
```

routes.py
```python

def init_routes(app):
    @app.route("/system/health")
    async def health_handler(request):
        return json(dict(status="ok"))


    @app.route("/system/stat")
    async def stat_handler(request):
        app.ctx.db.connect()
        msg_count = Message.select().count()
        app.ctx.db.close()
        return json(dict(msg_count=msg_count))
```

db.py
```python
from peewee import *

....

def init_db():
  ....
  return db
```

At first I did not realised that these issues are about the same but after some debugging with prints I have found that Sanic in multithreaded mode besides main instance of ``app`` creates two additional instances (I suppose I instance per thread) and these new instances in fact processing requests, and do not have routes and db defined.

After finding that behaviour I understand how to fix the issues - move all initialisation from "runtime" level to "module import" level

fixed code:
main.py
```python
from sanic import Sanic
from routes import app
from db import db

app.ctx.db = db

app.run(host='0.0.0.0', port=8765, debug=debug)
```

routes.py
```python

app = Sanic("pet project N7355")

@app.route("/")
async def index_handler(request):
    return json(dict(name="pet project N7355", version="0.1.0"))

@app.route("/system/health")
async def health_handler(request):
    return json(dict(status="ok"))

@app.route("/system/stat")
async def stat_handler(request):
    app.ctx.db.connect()
    msg_count = Message.select().count()
    app.ctx.db.close()
    return json(dict(msg_count=msg_count))
```

db.py
```python
from peewee import *

....

def init_db():
  ....
  return db

db = init_db()
```

from one hand this solution works but from other it looks ugly for me as with that code structure it hard to test modules code in isolation.

I completely understand that I used not the best approach with routes and better rewrite those with blueprints that available in Sanic, but still, for DB I have used approach listed on the Sanic site and is does not work for me because that Sanic's specifics.

As a result I have a open questions: 
- [rhetorical] Why it made is such way where only one way to make things right exists and this specifics is not listed in documentation?
- [practical] Maybe I did something wrong and things that I see as logical is not in fact logical?

Maybe someone faced something like this and know how to do that properly?