Why doesn't Flask establish persistent HTTP connection with multiple requests from Java application using HttpURLConnection?
Asked Answered
T

1

3

I have a minor project which will involve repeated HTTPS communication between a Java application and a Flask web server such that keeping the TCP connection alive is important. In order to test and understand the capabilities of these technologies, I have set up a simple GET request generator which sends multiple GET requests with time invertals to my simple Flask web server application and return a response. In such a setup, I would expect the TCP connection to be cached but I think my Flask application disconnects the connection after each response. Beware that I am quite rusty with Java and it is my first time getting acquainted with Flask.

My Java GET request generator is like below:

package com.deu;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class Main {

    public static void main(String[] args) {
        try {
            sendHttpRequests("http://127.0.0.1:5000", 1000, 5);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sendHttpRequests(String url, int interval, int count) throws IOException, InterruptedException {
        URL hostURL = new URL(url);

        for (int i = 0; i < count; i++) {
            HttpURLConnection httpConn = (HttpURLConnection) hostURL.openConnection();
            httpConn.setRequestMethod("GET");

            BufferedReader reader = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));
            StringBuilder response = new StringBuilder();
            String line;

            while ((line = reader.readLine()) != null)
                response.append(line);

            System.out.println("Response to i="+i+" "+response.toString());
            reader.close();

            Thread.sleep(interval);
        }
    }

}

My Flask web server application is like below:

import flask
from flask import Flask, request
from werkzeug.serving import WSGIRequestHandler

app = Flask(__name__)


@app.route('/', methods=['GET'])
def hello_world():
    resp = flask.Response("Hello World!")
    print(request.headers)
    print(resp.headers)

    return resp


if __name__ == '__main__':
    WSGIRequestHandler.protocol_version = "HTTP/1.1"
    app.run()

After the Java application terminates, the outputs I get for Java and Flask applications respectively are:

Response to i=0 Hello World!
Response to i=1 Hello World!
Response to i=2 Hello World!
Response to i=3 Hello World!
Response to i=4 Hello World!
FLASK_APP = app.py
FLASK_ENV = development
FLASK_DEBUG = 0
In folder B:/Development/Projects/Python/ProjectABC
B:\Development\Projects\Python\ProjectABC\venv\Scripts\python.exe -m flask run
 * Serving Flask app 'app.py'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
User-Agent: Java/17.0.1
Host: 127.0.0.1:5000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive


Content-Type: text/html; charset=utf-8
Content-Length: 12


127.0.0.1 - - [08/Apr/2023 01:26:28] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [08/Apr/2023 01:26:29] "GET / HTTP/1.1" 200 -
User-Agent: Java/17.0.1
Host: 127.0.0.1:5000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive


Content-Type: text/html; charset=utf-8
Content-Length: 12


User-Agent: Java/17.0.1
Host: 127.0.0.1:5000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive


Content-Type: text/html; charset=utf-8
Content-Length: 12


127.0.0.1 - - [08/Apr/2023 01:26:30] "GET / HTTP/1.1" 200 -
User-Agent: Java/17.0.1
Host: 127.0.0.1:5000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive


Content-Type: text/html; charset=utf-8
Content-Length: 12


127.0.0.1 - - [08/Apr/2023 01:26:31] "GET / HTTP/1.1" 200 -
User-Agent: Java/17.0.1
Host: 127.0.0.1:5000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive


Content-Type: text/html; charset=utf-8
Content-Length: 12


127.0.0.1 - - [08/Apr/2023 01:26:32] "GET / HTTP/1.1" 200 -

As can be seen, the requests receive responses as expected and even the request headers Flask application received shows the presence of Connection: keep-alive. But the reason I think a new TCP connection is created each time are that the response headers do not contain Connection: keep-alive and the output of netstat cmd command:

Active Connections

  Proto  Local Address          Foreign Address        State
  TCP    127.0.0.1:5000         DESKTOP-XXXXXXX:52821  TIME_WAIT
  TCP    127.0.0.1:5000         DESKTOP-XXXXXXX:52823  TIME_WAIT
  TCP    127.0.0.1:5000         DESKTOP-XXXXXXX:52824  TIME_WAIT
  TCP    127.0.0.1:5000         DESKTOP-XXXXXXX:52830  TIME_WAIT
  TCP    127.0.0.1:5000         DESKTOP-XXXXXXX:52831  TIME_WAIT

If my understanding of the output is correct (please correct me if I am wrong), 5 different TCP connections were made and now the OS is waiting to clear them, indicating keep-alive has failed. I searched other questions for answers and tried putting WSGIRequestHandler.protocol_version = "HTTP/1.1" but to no avail.

Note: I am trying to achieve persistent connection using HTTP first, after which I will apply it to HTTPS.

Terrazas answered 7/4, 2023 at 22:50 Comment(5)
It looks to me like your client code creates a new connection each time through the loop. I'd explore putting 'openConnection' before the loop. That means you'll need a solution to finding end-of-response other than reading until the end. Oh, and 'connection: keep-alive' is not an HTTP/1.1 thing. The assumption is that it's kept open unless 'connection: close', i.e., the reverse of HTTP/1.0.Fascination
@ArfurNarf I've tried it and it sounds like a solution to mimic persistent unidirectional server-to-client communication (Flask waits for the entire message before entering the 'hello_world' routine so only works for response side) but in my setup, I intend to also send multiple requests along with their responses. Does Java still send 'Connection: keep-alive' for backward compatibility, even if its using HTTP/1.1?Terrazas
I don't follow what you mean. But, to be clear, I have no trouble maintaining persistent connections to a Flask server. In my case, the client code is also Python (using http.client).Fascination
@ArfurNarf Werkzeug dropped support for persistent connections in early 2022, which explains the issues I had because I have been using the development WSGI server. Maybe you were using an earlier version of Werkzeug or using a deployment WSGI server. See my answer below.Terrazas
Hmm, I suppose I never noticed that change in werkzeug; but I no longer use it. Thanks.Fascination
T
1

I had been trying to achieve persistent connection using the WSGI server Flask comes packaged with called Werkzeug. When I switched to a deployment WSGI server, in my case Waitress, keep-alive started to work and TCP connections stopped being dropped. The issue was being caused by a change in Werkzeug implementation. Their change log for Version 2.1.2 released in 2022-04-28 states:

Disable keep-alive connections in the development server, which are not supported sufficiently by Python’s http.server. #2397

Werkzeug team removed the persistent connection support due to the shortcomings of http.server which it extends. While it is understandable that the lack of keep-alive function may not be essential in a development environment, it is frustrating that there is very little to no communication about this in the Internet.

Terrazas answered 9/4, 2023 at 13:14 Comment(1)
It's a shame they removed it. Some time back, I moved to code based directly on http.server, and it was absolutely clear to me from the http.server documentation that you must read the request body even if you did not need to see it (say, if status != success). Removing useful functionality from werkzeug because people can't read documentation seems like a good way to get people to not be using werkzeug.Fascination

© 2022 - 2024 — McMap. All rights reserved.