This question was recently asked by @LordOdin on the Discord server.
How can I make an http and https server in sanic with an auto https redirect
I am providing an answer here because:
- It will be easier to follow, and
- it will be easier for someone else to find in the future.
Of course, reverse proxying in NGINX is a solution. There are loads of tutorials online how to do that. But, how can we do it in Sanic?
First, we need to think about what needs to happen. HTTP requests are typically served by default on port 80
. And, a request using TLS encryption over HTTPS typically uses 443
.
So our solution must accomplish responding over two different ports whereby one port is redirecting towards the other.
Let’s get the easy step out of the way and setup two apps. I am using 9999
and 8888
as my ports, but there is nothing special about them. Swap in 443, and add SSL to the HTTPS app, and it will operate the same.
HTTP_PORT = 9999
HTTPS_PORT = 8888
http = Sanic("http")
http.config.SERVER_NAME = f"localhost:{HTTP_PORT}"
https = Sanic("https")
https.config.SERVER_NAME = f"localhost:{HTTPS_PORT}"
Now, let’s setup some routes the ones that we care about serving on HTTPS.
@https.get("/foo")
def foo(request):
return text("foo")
@https.get("/bar")
def bar(request):
return text("bar")
Our HTTP app also needs a route. It will only have one, and its job is to catch any request and redirect it. We will use sanic.response.redirect
to handle that.
In this example, we use app.url_for()
to rebuild the URL, and we explicitly tell it that we want it to point to the SERVER_NAME
of our other server. Also important is setting the _scheme
. In my simple example where I am not actually using encryption, I left it as _scheme="http"
, but to implement this for real, you would switch that to https
.
@http.get("/<path:path>")
def proxy(request, path):
url = request.app.url_for(
"proxy",
path=path,
_server=https.config.SERVER_NAME,
_external=True,
_scheme="http",
)
return response.redirect(url)
The last part comes in wiring it up. I am going to use two brand new listeners that will be available in v21.3, but you could use listener("before_server_start")
and listener("after_server_end")
as well.
Basically, we will use the create_server
pattern and store the alternative server instance on our main HTTPS app. When it starts up, it will also start the secondary app, and take care of its lifecycle as well.
@https.listener("main_process_start")
async def start(app, _):
global http
app.http_server = await http.create_server(
port=HTTP_PORT, return_asyncio_server=True
)
app.http_server.after_start()
@https.listener("main_process_stop")
async def stop(app, _):
app.http_server.before_stop()
await app.http_server.close()
app.http_server.after_stop()
Putting it all together:
from sanic import Sanic, response, text
HTTP_PORT = 9999
HTTPS_PORT = 8888
http = Sanic("http")
http.config.SERVER_NAME = f"localhost:{HTTP_PORT}"
https = Sanic("https")
https.config.SERVER_NAME = f"localhost:{HTTPS_PORT}"
@https.get("/foo")
def foo(request):
return text("foo")
@https.get("/bar")
def bar(request):
return text("bar")
@http.get("/<path:path>")
def proxy(request, path):
url = request.app.url_for(
"proxy",
path=path,
_server=https.config.SERVER_NAME,
_external=True,
_scheme="http",
)
return response.redirect(url)
@https.listener("main_process_start")
async def start(app, _):
global http
app.http_server = await http.create_server(
port=HTTP_PORT, return_asyncio_server=True
)
app.http_server.after_start()
@https.listener("main_process_stop")
async def stop(app, _):
app.http_server.before_stop()
await app.http_server.close()
app.http_server.after_stop()
https.run(port=HTTPS_PORT, debug=True)