How to get embedded Bokeh server doc to report page error to flask server

Hi,
I have a Flask plus Bokeh server setup where the two servers are started by their own scripts (python flask_app.py and bokeh serve ...) and the Bokeh plots are embedded in pages by templates using server_document. In case there is something wrong with the Bokeh server I would like to capture any page error from the Bokeh server by showing a Flask error template. I have tried to add routes like @app.errorhandler(403) but also @app.teardown_request in order to try to capture any page error but so far no luck. If I understand correctly as long as flask is able to create the embedded template (the html doc) then that would not return any page error.

To show what I mean I have taken the Flask embed example from git (bokeh/examples/embed/arguments at branch-2.2 · bokeh/bokeh · GitHub) and used that. In the flask_server.py script I have added some error handling routes but they are not run if Bokeh server reports a eg 403 GET error.

bokeh_server.py

#!/usr/bin/env python

'''This example demonstrates embedding an autoloaded Bokeh server
into a simple Flask application, and passing arguments to Bokeh.

To view the example, run:

    python flask_server.py

in this directory, and navigate to:

    http://localhost:5000

'''
import numpy as np

from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Slider, TextInput
from bokeh.plotting import figure

# Retrieving the arguments
args = curdoc().session_context.request.arguments

try:
    batchid = int(args.get('batchid')[0])
except (ValueError, TypeError):
    batchid = 1

func = {
    1 : np.cos,
    2 : np.sin,
    3 : np.tan
}[batchid]

# Set up data
N = 200
x = np.linspace(0, 4*np.pi, N)
y = func(x)
source = ColumnDataSource(data=dict(x=x, y=y))

# Set up plot
plot = figure(plot_height=400, plot_width=400, title="my wave",
              tools="crosshair,pan,reset,save,wheel_zoom",
              x_range=[0, 4*np.pi], y_range=[-2.5, 2.5])

plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

# Set up widgets
text = TextInput(title="title", value="Batch n°{}".format(batchid))
offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0)
phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1)

# Set up callbacks
def update_title(attrname, old, new):
    plot.title.text = text.value

text.on_change('value', update_title)

def update_data(attrname, old, new):

    # Get the current slider values
    a = amplitude.value
    b = offset.value
    w = phase.value
    k = freq.value

    # Generate the new curve
    x = np.linspace(0, 4*np.pi, N)
    y = a*func(k*x + w) + b

    source.data = dict(x=x, y=y)

for w in [offset, amplitude, phase, freq]:
    w.on_change('value', update_data)

# Set up layouts and add to document
inputs = column(text, offset, amplitude, phase, freq)

curdoc().add_root(row(inputs, plot, width=800))

Started with

bokeh serve bokeh_server.py --allow-websocket-origin=127.0.0.1:5000

flask_server.py

'''This example demonstrates embedding an autoloaded Bokeh server
into a simple Flask application, and passing arguments to Bokeh.

To view the example, run:

    python flask_server.py

in this directory, and navigate to:

    http://localhost:5000

'''
import atexit
import subprocess

from flask import Flask, render_template_string

from bokeh.embed import server_document

home_html = """
<!DOCTYPE html>
<html lang="en">
  <body>
    <div class="bk-root">
      <h1><a href="/batch/1"> Batch 1 (cos)</a></h1>
      <h1><a href="/batch/2"> Batch 2 (sin)</a></h1>
      <h1><a href="/batch/3"> Batch 3 (tan)</a></h1>
    </div>
  </body>
</html>
"""

app_html = """
<!DOCTYPE html>
<html lang="en">
  <body>
    <div>
        <h2><a href="/batch/1">Batch 1 (cos)</a> - <a href="/batch/2">Batch 2 (sin)</a> - <a href="/batch/3">Batch 3 (tan)</a></h2>
    </div>
    {{ bokeh_script|safe }}
  </body>
</html>
"""

err_html = """
<!DOCTYPE html>
<html lang="en">
  <body>
    <div>
        <h2>{{ error }}</h2>
    </div>
  </body>
</html>
"""

app = Flask(__name__)

bokeh_process = subprocess.Popen(
    ['python', '-m', 'bokeh', 'serve', '--allow-websocket-origin=localhost:5000', 'bokeh_server.py'], stdout=subprocess.PIPE)

@atexit.register
def kill_server():
    bokeh_process.kill()

@app.route('/')
def home():
    return render_template_string(home_html)

@app.route('/batch/<int:batchid>')
def visualization(batchid):
    bokeh_script = server_document(url='http://localhost:5006/bokeh_server', arguments=dict(batchid=batchid))
    return render_template_string(app_html, bokeh_script=bokeh_script)

@app.errorhandler(404)
def page_not_found(e):
  return render_template_string(err_html, error = "404 Not found")

@app.errorhandler(403)
def page_not_found(e):
  return render_template_string(err_html, error = "403 Forbidden")

@app.teardown_request
def teardown_request_func(error=None):
   print("teardown_request is running")
   if error:
      # log error
      print(str(error))


if __name__ == '__main__':
    app.run(debug=True)

Flask server started with python flask_server.py

When running this and going to localhost:5000, when one chooses a link I get to the app_html page but no embedded plot is shown due to 403 page error reported by Bokeh server. I would like to capture this error and show to the user that there is a page error - how do I do that?

Firefox console:

Request URL:http://localhost:5006/bokeh_server/ws?bokeh-protocol-version=1.0&bokeh-session-id=MgpZNLFpFlZDDacVqZzjLlWZHmcjCngpWn19QiIZnyXs
Request method:GET
Remote address:127.0.0.1:5006
Status code:
403
Version:HTTP/1.1

Have tested in Bokeh version 1.3.4 (py2.7) and 2.1.1 (py3.6.8), both on Redhat 7.

I don’t think there is any straightforward built-in way at present. The server_document call ultimately provides this snippet of JavaScript to execute an XMLHttpRequest that to get the code from the Bokeh server for actually embedding a session. As you can see, it only defines an onload and doesn’t have any error handling hooks set up. It probably should, and it’s probably not too difficult, but surprisingly no one has ever asked about it before. In the mean time. I’d encourage you to file a GitHub issue to discuss adding it. In the mean time, I think you would have to copy the code for server_document and modify it to supply an updated JS autoload tag that does define and onerror to do whatever you want to happen there.

@Bryan, thanks for taking the time looking into this. I have now opened an issue on GitHub

I have the same need as you, Jonas, and this is the workaround I’ve come up with. It’s not pretty, but it works:

import request
    @app.route('/path/<arg>', methods=['GET'])
    def bkapp_page(arg):
        script = server_document(f"http://{IP_ADDRESS}:{IP_PORT}",
                                 arguments={"arg": arg})

        # Get the path to the Bokeh Server from the <script> tag
        bs = re.compile("<script src=\"(.*)\" id.*")
        m = bs.match(script.strip())
        bokeh_server_address = m.group(1)
        r = requests.get(bokeh_server_address)
        # Return to homepage if there is an error with the Bokeh Server, but you could redirect to an error page
        if r.status_code != 200:
            return redirect("/")

        return render_template("template.html", script=script, my_arg=arg)
1 Like