Server Sent events with Flask and Google App Engine - google-app-engine

I've been trying to get a webapp to work which uses Server-Sent Events. The app that I've written works on my local machine when using Flask's app.run() method. But when I run it on GAE, I've not been able to make it work.
The webapp uses SSE to publish a message with the current time every so often. The client simply adds it to the HTML of a div.
Flask app
import random
from datetime import datetime
from flask import render_template, Response
from time import sleep
from message_server import app
def event_stream():
while True:
time_now = datetime.now()
message = "New message at time: {0}".format(time_now.strftime("%H:%M:%S"))
yield "event: {0}\ndata: {1}\n\n".format("listen", message)
sleep(random.randint(1, 5))
#app.route('/')
def hello():
return render_template('home.html')
#app.route('/stream')
def stream():
return Response(event_stream(), mimetype="text/event-stream")
Javascript in home.html
var source = new EventSource("/stream");
source.onmessage = function(event) {
document.getElementById("messages").innerHTML += event.data + "<br>";
};
source.addEventListener("listen", function(event) {
document.getElementById("messages").innerHTML += event.data + "<br>";
}, false);
GAE app.yaml
runtime: python
env: flex
entrypoint: gunicorn -b :$PORT --worker-class gevent --threads 10 message_server:app
runtime_config:
python_version: 3
manual_scaling:
instances: 1
resources:
cpu: 1
memory_gb: 0.5
disk_size_gb: 10
My directory structure is as following:
app.yaml
/message_server
__init__.py
sse.py
/templates
home.html
message_server is the package that contains the flask app object.
I am using Firefox 67 to test my app.
In the networking tab of Firefox developer console, I see a GET request made to /stream, but no response received even after a minute.
In the GAE logs, I am seeing "GET /stream" 499.
How do I figure out what's wrong?

I found the answer while browsing Google App Engine's documentation - on this page: https://cloud.google.com/appengine/docs/flexible/python/how-requests-are-handled
Essentially, you want the following header in the HTTP response for SSE to work:
X-Accel-Buffering: no
This disables buffering that is enabled by default. I tested it and SSE is working as expected for me.

Related

Running into issues with CORS with Flask

I have searched for hours trying to find a decent solution to my problem and I can't seem to find a solution. Basically, I broke up my Flask API into two separate files using blueprints, one that serves as the main Flask application and the other to handle all the authentication routes. I am running into CORS issues... Before I separated them, it worked fine with the flask-cors library. However, when I broke it into separate files using blueprints, it didn't work. I did a bunch of research and there seems be a couple ways to do it, but the best way seems to be solving the problem on the server side (at least in theory because I haven't found a solution to it yet).
app.py
# Third-party libraries
from flask import Flask
from flask_cors import CORS
# Internal imports
from login import login_page
# Flask app setup
app = Flask(__name__)
app.register_blueprint(login_page)
CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'
if __name__ == "__main__":
app.run(host="localhost", debug=True)
login.py
# Third-party libraries
from flask import Flask, redirect, request, url_for, Blueprint, render_template, abort
from flask_login import (
LoginManager,
current_user,
login_required,
login_user,
logout_user
)
from oauthlib.oauth2 import WebApplicationClient
import requests
from flask_cors import CORS
login_page = Blueprint('login_page', __name__)
CORS(login_page)
# Login route
#login_page.route('/login', methods=['GET'])
def login():
# Find out what URL to hit for Google login
google_provider_config = get_google_provider_config()
authorization_endpoint = google_provider_config["authorization_endpoint"]
# Use library to construct the request for Google login and provide
# scopes that let you retrieve user's profile from Google
request_uri = client.prepare_request_uri(
authorization_endpoint,
redirect_uri = request.base_url + "/callback",
scope = ["openid", "email", "profile"]
)
return redirect(request_uri)
From the documentation:
https://flask-cors.readthedocs.io/en/latest/api.html#using-cors-with-blueprints
Which seems to be what I am doing... But it's not working.
Edit:
React code:
authenticate = async () => {
console.log('authenticate called')
const response = await users.get('/login');
}
I added a test route to see if the way I was handling blue prints was incorrect with the flask-cors library as follows:
#login_page.route('/test')
def test():
print('test')
return 'test'
Sure enough, I was able to see the text print on the console. So this leaves me to believe it is something else preventing me from accessing Google's authentication servers.
Flask application is running on localhost:5000 and React application is running on localhost:3000. The string that gets passed into the redirect method in login is as follows:
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=None&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Flogin%2Fcallback&scope=openid+email+profile
However, this was working prior to working with React.
Chrome console error:
Access to XMLHttpRequest at '...' (redirected from 'http://localhost:3000/login') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I have also added a proxy to my package.json file as follows:
"proxy": "http://localhost:5000"
Which I got from: https://blog.miguelgrinberg.com/post/how-to-create-a-react--flask-project/page/3
Update:
I have decided to go with the approach of solving this problem using Nginx since it seems to that flask-cors library is not suitable for this problem as no solution seems to be possible.
Nginx.conf
user www-data;
worker_processes 1;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
}
http {
upstream react-client {
server react-client;
}
upstream flask-backend {
server flask-backend;
}
server {
listen 80;
server_name localhost;
location / {
# From docker-compose, react-client port 80 is exposed
proxy_pass http://react-client;
}
# location /login/callback
# location /logout
}
server {
listen 3000;
server_name localhost;
location /login {
proxy_pass http://flask-backend;
}
}
}
docker-compose.yml:
version: '3'
services:
reverse-proxy:
image: nginx:1.17.10
container_name: reverse-proxy
depends_on:
- flask-backend
- react-client
volumes:
- ./reverse-proxy/nginx.conf:/etc/nginx/nginx.conf
ports:
- 80:80
react-client:
image: react-client
container_name: react-client
build:
context: ./client
depends_on:
- flask-backend
ports:
- 3000:3000
restart: on-failure
flask-backend:
image: flask-backend
container_name: flask-backend
build:
context: ./api
ports:
- 5000:5000
restart: on-failure
With the Nginx configuration and Docker files, I am able to go to http://localhost:80 which forwards me to the react client. This time, I am not getting the CORS errors, but I do get this message in my inspector, Failed to load resource: The network connection was lost.
My understanding is this:
http://localhost:80 -> http://localhost:3000, then when a button is pressed, it prompts the new route
http://localhost:3000/login -> http://localhost:5000/login
I have two server blocks because the web application is running in port 3000 and Nginx server on port 80.
Instead of CORS(login_page) try CORS(login_page, resources=r'/login')
Edit: To explain why this might work.
The error you're getting indicates that the /login route doesn't have CORS permitted. You're instantiating Flask-CORS correctly so I suspect if you tried delineating the specific route you want CORS to apply to you would fix the issue.
The docs make it seem like CORS(login_page) should automatically open up all routes to CORS but because that's not working trying the above answer might provide better results.

How do I force google app engine to use https ssl protocol?

I have a flask web app deployed to google app engine. However, it is not forcing my link to https. However, if I refresh it, it will go to the ssl https version. But the user can still remove the s and jump back into the http version. Is there any way to completely remove the http protocol on my site and have it redirect to the ssl version. Here is the app.yaml file I'm using currently. I also tried adding in redirect_http_response_code: 301 with no luck to remove the http protocol
runtime: python
env: flex
entrypoint: gunicorn -b :$PORT main:app
runtime_config:
python_version: 3.7
# This sample incurs costs to run on the App Engine flexible environment.
# The settings below are to reduce costs during testing and are not appropriate
# for production use. For more information, see:
# https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml
manual_scaling:
instances: 1
resources:
cpu: 1
memory_gb: 0.5
disk_size_gb: 10
handlers:
- url: /.*
secure: always
script: auto
I prefer not to install additional software packages for relatively simple things, so I do it myself. For GAE flex there are a few things to handle. I've added comments below to help explain.
#app.before_request
def redirect_http():
# http -> https
if (
# Flask tells us when request is http. This might not be needed for you but
# I need it because I use this code for GAE standard as well.
not request.is_secure and
# Load balancers forward https requests as http but set headers to let you
# know that original request was https
not request.headers.get('X-Forwarded-Proto') == 'https' and
# GAE cron urls must be http
not request.path.startswith("/cron")
):
return redirect("https" + request.url[4:], code=301)
# naked domain -> www
if request.url.startswith("https://example.com"):
return redirect('https://www.' + request.url[8:], code=301)
The Flask packages recommended by #tzovourn do other important things as well so you may want to consider those (I personally do all those things myself since it isn't hard to do them).
I noticed that you are using App Engine Flexible. As per documentation, setting secure: always in app.yaml doesn't work for App Engine Flexible.
Documentation recommends to secure your HTTP requests by using the Flask Talisman library.
Another way to configure your Flask application to redirect all incoming requests to HTTPS is to use the Flask-SSLify extension

Google App Engine Flexible Environment at 0 instances

Over the past week I've been seeing the number of instances on my GAE Flexible Environment fall to 0, with no new instance spinning up. My understanding of the Flexible environment is that this shouldn't be possible... (https://cloud.google.com/appengine/docs/the-appengine-environments)
I was wondering if anyone else has been seeing these issues, or if they've solved the problem on their end before. My one hypothesis is that this might be an issue with my health monitoring endpoints, but haven't seen anything that jumps out as a problem when I review the code.
This hasn't been a problem for me until last week, and now it seems like I have to redeploy my environment (with no changes) every couple of days just to "reset" the instances. It's worth noting that I have two services under this same App Engine project, both running flexible versions. But I only seem to have this issue with one of the services (what I call the worker service).
Screenshot from App Engine UI:
Screenshot from Logs UI that shows the SIGTERM being sent:
PS - Could this have anything to do with the recent Google Compute issues that have been coming up... https://news.ycombinator.com/item?id=18436187
Edit: Adding the yaml file for "worker" service. Note that I'm using Honcho to add an endpoint to monitor health of the worker service via Flask. I added those code examples as well.
yaml File
service: worker
runtime: python
threadsafe: yes
env: flex
entrypoint: honcho start -f /app/procfile worker monitor
runtime_config:
python_version: 3
resources:
cpu: 1
memory_gb: 4
disk_size_gb: 10
automatic_scaling:
min_num_instances: 1
max_num_instances: 20
cool_down_period_sec: 120
cpu_utilization:
target_utilization: 0.7
Procfile for Honcho
default: gunicorn -b :$PORT main:app
worker: python tasks.py
monitor: python monitor.py /tmp/psq.pid
monitor.py
import os
import sys
from flask import Flask
# The app checks this file for the PID of the process to monitor.
PID_FILE = None
# Create app to handle health checks and monitor the queue worker. This will
# run alongside the worker, see procfile.
monitor_app = Flask(__name__)
#monitor_app.route('/_ah/health')
def health():
"""
The health check reads the PID file created by tasks.py main and checks the proc
filesystem to see if the worker is running.
"""
if not os.path.exists(PID_FILE):
return 'Worker pid not found', 503
with open(PID_FILE, 'r') as pidfile:
pid = pidfile.read()
if not os.path.exists('/proc/{}'.format(pid)):
return 'Worker not running', 503
return 'healthy', 200
#monitor_app.route('/')
def index():
return health()
if __name__ == '__main__':
PID_FILE = sys.argv[1]
monitor_app.run('0.0.0.0', 8080)

deploying tornado on Google App Engine

I am trying to deploy my python pricing module which takes product details (string) as argument, on to GAE. The tornado wrapper is working fine on localhost (localhost:8888/?q=) but giving server error 500 on GAE.
Code in Pricing-OOP.py file:
class MainHandler(tornado.web.RequestHandler):
def get(self):
q = self.get_query_argument("q")
res = Pricing(q).pricing()
self.write(json.dumps(res))
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
],debug=True)
if __name__ == '__main__':
Pickleload()
app = make_app()
container = tornado.wsgi.WSGIContainer(app)
http_server = tornado.httpserver.HTTPServer(container)
http_server.listen(8888)
tornado.ioloop.IOLoop.current().start()
app.yaml file:
service: tornado
runtime: python27
threadsafe: no
handlers:
- url: /.*
script: Pricing-OOP.py
The gcloud app logs tail is as follows:
2017-07-26 03:03:30 tornado[20170726t082447] "GET / HTTP/1.1" 500
2017-07-26 03:03:30 tornado[20170726t082447] "GET /favicon.ico HTTP/1.1" 500
2017-07-26 03:03:33 tornado[20170726t082447] "GET / HTTP/1.1" 500
2017-07-26 03:03:34 tornado[20170726t082447] "GET /favicon.ico HTTP/1.1" 500
How do I correct this?
There are a few notes regarding deploying on Google App Engine in the Tornado docs.
In particular, a Tornado application on GAE must be run as a WSGI application. You cannot do things like open ports on the local machine,
and unfortunately it also prevents the use of asynchronous aspects of Tornado (which are often the main driver for using it in the first place).
In your case, instead of creating the HttpServer yourself, you should just create the WSGIAdapter:
# Pricing-OOP.py
# ...
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
],debug=False)
application = make_app()
application = tornado.wsgi.WSGIAdapter(application)
You then tell GAE where to find application by referencing it in the script directive of your configuration file:
# app.yaml
# ...
handlers:
- url: /.*
script: Pricing-OOP.application
Because you are now a "pure" WSGI application, you need to run it in a container. The Google Cloud SDK includes a development server in dev_appserver.py that can be used to host your app:
$ dev_appserver.py . # in directory containing app.yaml
That being done, you can run the same application code both locally and in your GAE instance.

Google App Engine service (module) not starting, and flooding 404's to /_ah/start

I'm refactoring an existing codebase. I switched from using the appcfg.py to using the gcloud command, which seemed to go fine. Our entire codebase was running on one default frontend instance, which I'm now trying to break into services. To start, I created one "worker" backend service, and I'm using a cron job to test.
I can see the worker in the console, but no instance is started. The logs for that service are rapidly flooded with 404's to /_ah/start. I've tried manual and basic scaling. The documentation states that it's okay not to have a startup script, and that a 404 at that endpoint is considered success. However, the instance is not starting.
Logs
worker.yaml
service: worker
runtime: python27
api_version: 1
instance_class: B2
manual_scaling:
instances: 1
threadsafe: false
handlers:
- url: /work/.*
script: worker.app
secure: always
login: admin
worker.py
import webapp2
import handlers
config = {
#...
}
app = webapp2.WSGIApplication([
webapp2.Route(
'/work/test<:/?>',
handlers.Test,
methods=['GET'],
),
], debug=True, config=config)
dispatch.yaml
dispatch:
- url: "*/work/*"
module: worker

Resources