1

In my app I want to serve a PNG image generated from a numpy array sent via the POST method, so in the app I came up with there are two routes - the one that serves HTML with the <img /> tag and the other that generates it. Unfortunately the app times out after sending the POST request and a 500 error is displayed instead of the image:

from flask import Flask, request, url_for, Response
import requests
from matplotlib import pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg
import numpy as np
import json
from io import BytesIO

app = Flask(__name__)

@app.route('/img/show')
def show_html():
        data = np.random.rand(80).reshape((2, 40))
        img = BytesIO(requests.post(request.scheme + '://' + request.host + url_for('img_gen'), json=json.dumps(data.astype(float).tolist())).raw.read())
        return '<img src="{}" />'.format(img.getvalue())


@app.route('/img/gen', methods=['POST'])
def img_gen():
        data = np.array(json.loads(request.get_json()))
        fig = plt.figure()
        plt.plot(data[0, :], c='b')
        plt.plot(data[1, :]*-1, c='r')
        plt.grid()
        png = BytesIO()
        FigureCanvasAgg(fig).print_png(png)
        plt.close(fig)
        return Response(png.getvalue(), mimetype='image/png')

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

I checked that the array is correctly passed to and received by the /img/gen route, but it seems like the app gets stuck at or before the return line. Gunicorn v3 does not print any error message except for the WORKER TIMEOUT. I would greatly appreciate if someone pointed the cause of this problem to me.

mac13k
  • 2,423
  • 23
  • 34
  • Your last remark suggests that either you sent an unexpected data to `Response`. Have you tested `img_gen` function separately to see if it can show images saved as a static file ? Not a response to your question but have you considered saving the image to a temporary file and then serving it [`send_file`](https://flask.palletsprojects.com/en/1.1.x/api/#flask.send_file) ? – Kaan E. Jan 05 '20 at 23:40
  • @Kaan E. I tested this code and similar function worked for PNG images generated upon GET request. I learnt this code from [here](https://gist.github.com/illume/1f19a2cf9f26425b1761b63d9506331f) – mac13k Jan 06 '20 at 11:28

1 Answers1

1

There are a couple problems with your code:

  1. You are using .raw.read() to directly access the byte response, but since the response is of mimetype "img/png", you can simply use .content. I wasn't able to get .raw.read() to work, so I would suggest using `.

  2. You are mixing string and byte types. Normally, whenever bytes are evaluated to a string, b' is appended at the beginning and ' at the end. Additionally, the python byte type is not base64, use b64encode from the base64 module

  3. You need to begin src= with the URI type. Based on 2, 3 and 4, you should write this: b'<img src="data:image/png;base64,%b" />' %b64encode(img), which uses the byte type, declares the data type, and interpolates a base64 bytestring.

The full code here:

from flask import Flask, request, url_for, Response
import requests
from matplotlib import pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg
import numpy as np
import json
from io import BytesIO
from base64 import b64encode

app = Flask(__name__)


@app.route("/img/show")
def show_html():
    data = np.random.rand(80).reshape((2, 40))
    img = requests.post(
        request.scheme + "://" + request.host + url_for("img_gen"),
        json=json.dumps(data.astype(float).tolist()),
    ).content
    return b'<img src="data:image/png;base64,%b" />' %b64encode(img)


@app.route("/img/gen", methods=["POST"])
def img_gen():
    data = np.array(json.loads(request.get_json()))
    fig = plt.figure()
    plt.plot(data[0, :], c="b")
    plt.plot(data[1, :] * -1, c="r")
    plt.grid()
    png = BytesIO()
    FigureCanvasAgg(fig).print_png(png)
    plt.close(fig)
    return Response(png.getvalue(), mimetype="image/png")


if __name__ == "__main__":
    app.run()

Finally, I will note that you don't necessarily need to use a separate endpoint for the image, since the endpoint is used only by the server itself. Assuming that you are not calling an external server, it would make more sense to have a function returning the image, called by the show_html function.

FoolsWisdom
  • 821
  • 4
  • 6
  • Thanks for the answer, however the code you proposed still does not work for me - I keep getting worker timeouts every time I hit the `/img/show` endpoint. I am using `gunicorn-3 (version 19.9.0)`. – mac13k Jan 06 '20 at 11:40
  • @mac13k, I am currently on windows, so I can't test gunicorn (I tested with flask's built in server). However, [this question](https://stackoverflow.com/questions/10855197/gunicorn-worker-timeout-error) may be helpful. It guess the worker timeout is because it takes too long to produce the image and send the response. – FoolsWisdom Jan 06 '20 at 13:09
  • I wrote another app that generates matplotlib plots in PNG on GET requests and it works fine - no timeouts and images are fine. There I could include the URL to the endpoint in `` directly without the need of storing or processing the response, so I am guessing that in this case there is something wrong with the way we are trying to display `requests.post().content` in HTML. – mac13k Jan 06 '20 at 13:35
  • I modified the other app that worked fine when serving the image directly from the URL - with `requests.get()` it started timing out, so this is where the problem occurs, but I cannot figure out why... – mac13k Jan 06 '20 at 14:26
  • 1
    @mac13k, I just tested with gunicorn, and I found the problem. Running `gunicorn main:app` only uses a single worker by default - meaning that it can only handle *one* request at a time, but for this code to work, you need to run *two* workers at least, since you need both endpoints to be called at the same time. So what happens is that the `show` endpoint waits for the `gen` endpoint forever. Try running `gunicorn -w 2 main:app` and it should work. [see here](https://docs.gunicorn.org/en/stable/settings.html#workers). – FoolsWisdom Jan 06 '20 at 14:35
  • Yes, that did it! Thanks for your help. – mac13k Jan 06 '20 at 14:38