[Bug] Secondary apps lack built-in HTTP signals (NotFound error) when receiving requests in spawn / dev mode
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
When running multiple Sanic applications using app.prepare() and Sanic.serve(primary=app) under dev=True (or any configuration that forces the spawn multiprocessing context), secondary applications fail to register their built-in HTTP signals in the spawned worker process.
When an HTTP request hits the secondary app's port, the worker process crashes immediately with a sanic_routing.exceptions.NotFound error because the underlying signal router is completely empty.
To Reproduce
Here is a Minimal Reproducible Example (MRE).
I've included a custom managed worker process (via app.manager.manage) that automatically sends an HTTP request to the secondary app a few seconds after startup to easily trigger the bug.
import time
from urllib import request
from sanic import Sanic
from sanic.response import text
app = Sanic("PrimaryApp")
app_secondary = Sanic("SecondaryApp")
@app.get("/")
async def primary_handler(req):
return text("Primary OK")
@app_secondary.get("/")
async def secondary_handler(req):
return text("Secondary OK")
# A custom background process to automatically trigger the bug
def custom_worker_process():
print("--- Wait 3 seconds for servers to start...")
time.sleep(3)
print("--- Sending request to secondary app...")
try:
# This will INSTANTLY trigger the NotFound crash in the server process!
response = request.urlopen("http://127.0.0.1:8001/")
html_content = response.read().decode('utf-8')
print(f"--- Request SUCCESS! Response content: '{html_content}'")
except Exception as e:
print(f"--- ❌ Request failed due to server crash: {e}")
@app.main_process_ready
async def attach_custom_worker(app_: Sanic, _):
# Spawn the custom process
app_.manager.manage("trigger-bot", custom_worker_process, {}, workers=1)
if __name__ == "__main__":
app.prepare(port=8000, dev=True)
app_secondary.prepare(port=8001, dev=True)
Sanic.serve(primary=app)
Steps:
- Run the script above.
- Wait 3 seconds for the
trigger-botprocess to send the request to port 8001.
Traceback / Output: Instead of a successful response, the background worker reports a 500 error:
--- Wait 3 seconds for servers to start...
--- Sending request to secondary app...
--- ❌ Request failed due to server crash: HTTP Error 500: Internal Server Error
And the Sanic server console raises the following exception:
sanic_routing.exceptions.NotFound: Could not find signal http.lifecycle.begin
...
sanic_routing.exceptions.NotFound: Could not find signal http.lifecycle.complete
Root Cause Analysis
I dug into the source code (sanic/mixins/startup.py and sanic/worker/serve.py) and found the missing link during the worker spawn lifecycle:
- In
worker_serve, only the primary app is passed to_serve_http_1(), which eventually triggersapp._setup_server()andapp.register_builtins(). Thus, the primary app gets its full signal tree. - Secondary apps are launched via the
app._start_servershook attached tobefore_server_start. - Inside
_run_server(which is called by_start_serversfor secondary apps), the framework executesawait server_info.server.startup(), but it never callsregister_builtins()or_setup_server()for these secondary apps. As a result, thesignal_routerfor non-primary apps remains completely empty in the spawned process.
Current Workaround
To bypass this issue without patching the framework itself, I currently have to inject dummy handlers into the secondary app in the global scope before __main__. This forces the worker process to compile the required signal tree:
def patch_sanic_secondary_app(app: Sanic) -> Sanic:
"""
Workaround: Pre-register dummy handlers for core HTTP signals
to prevent NotFound crashes in spawn mode.
"""
async def _dummy_signal_handler(*args, **kwargs):
pass
core_events = [
"http.lifecycle.begin", "http.lifecycle.read_head", "http.lifecycle.request",
"http.lifecycle.handle", "http.lifecycle.read_body", "http.lifecycle.response",
"http.lifecycle.complete", "http.lifecycle.exception",
"http.routing.before", "http.routing.after",
"http.handler.before", "http.handler.after"
]
for event in core_events:
try:
app.add_signal(_dummy_signal_handler, event)
except Exception:
pass
for attach in ["request", "response"]:
try:
app.add_signal(_dummy_signal_handler, "http.middleware.before", condition={"attach_to": attach})
app.add_signal(_dummy_signal_handler, "http.middleware.after", condition={"attach_to": attach})
except Exception:
pass
return app
# Usage:
# app_secondary = patch_sanic_secondary_app(Sanic("SecondaryApp"))
Expected behavior
Sanic's worker manager should ensure that register_builtins() (or the equivalent initialization) is automatically executed for all prepared apps assigned to a worker, not just the primary app, so that secondary apps can handle HTTP requests properly in spawn mode.
Environment
- OS: Linux / Windows (Any OS using
spawnmultiprocessing context) - Sanic Version: 23.x / 24.x / 25.x (Tested on 25.12.1)
### Code snippet
_No response_
### Expected Behavior
_No response_
### How do you run Sanic?
Sanic CLI
### Operating System
Linux
### Sanic Version
25.12.1
### Additional context
_No response_Source: sanic-org/sanic