<p dir="auto">A tiny Flask app with a PUT request is behaving strangely: after it sends the response body that it is supposed to send (from my code), it then sends a second response body with a Flask error in it. The core of the error response is "Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method."</p>
<p dir="auto">It looks as though something is parsing the PUT request body as the beginning of a second HTTP request on the same TCP connection.</p>
<p dir="auto">Here is my app to reproduce the problem (`flaskdouble.py):</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="import flask

app = flask.Flask('flaskdouble')

@app.route('/foo', methods=['PUT', 'POST'])
def foo():
    return 'simple response\n'"><pre class="notranslate"><code class="notranslate">import flask

app = flask.Flask('flaskdouble')

@app.route('/foo', methods=['PUT', 'POST'])
def foo():
    return 'simple response\n'
</code></pre></div>
<p dir="auto">I'm running it in a Flask working dir, on the 2.1.x branch:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="FLASK_APP=flaskdouble FLASK_ENV=development ./env/bin/flask run --port 4080"><pre class="notranslate"><code class="notranslate">FLASK_APP=flaskdouble FLASK_ENV=development ./env/bin/flask run --port 4080
</code></pre></div>
<p dir="auto">To see the problem, I needed to attach a packet sniffer to the connection. For example, ngrep can do it with</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="sudo ngrep -d lo -W byline port 4080"><pre class="notranslate"><code class="notranslate">sudo ngrep -d lo -W byline port 4080
</code></pre></div>
<p dir="auto">Now with both <code class="notranslate">flaskdouble.py</code> and ngrep running, send a request to it:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="curl -v -X PUT -H 'content-type: text/plain' -d 'hello' http://localhost:4080/foo
*   Trying 127.0.0.1:4080...
* Connected to localhost (127.0.0.1) port 4080 (#0)
&gt; PUT /foo HTTP/1.1
&gt; Host: localhost:4080
&gt; User-Agent: curl/7.74.0
&gt; Accept: */*
&gt; content-type: text/plain
&gt; Content-Length: 5
&gt; 
* upload completely sent off: 5 out of 5 bytes
* Mark bundle as not supporting multiuse
&lt; HTTP/1.1 200 OK
&lt; Server: Werkzeug/2.1.1 Python/3.9.7
&lt; Date: Sat, 16 Apr 2022 00:10:10 GMT
&lt; Content-Type: text/html; charset=utf-8
&lt; Content-Length: 16
&lt; 
simple response
* Connection #0 to host localhost left intact"><pre class="notranslate"><code class="notranslate">curl -v -X PUT -H 'content-type: text/plain' -d 'hello' http://localhost:4080/foo
*   Trying 127.0.0.1:4080...
* Connected to localhost (127.0.0.1) port 4080 (#0)
&gt; PUT /foo HTTP/1.1
&gt; Host: localhost:4080
&gt; User-Agent: curl/7.74.0
&gt; Accept: */*
&gt; content-type: text/plain
&gt; Content-Length: 5
&gt; 
* upload completely sent off: 5 out of 5 bytes
* Mark bundle as not supporting multiuse
&lt; HTTP/1.1 200 OK
&lt; Server: Werkzeug/2.1.1 Python/3.9.7
&lt; Date: Sat, 16 Apr 2022 00:10:10 GMT
&lt; Content-Type: text/html; charset=utf-8
&lt; Content-Length: 16
&lt; 
simple response
* Connection #0 to host localhost left intact
</code></pre></div>
<p dir="auto">That looks fine ... until we look at the traffic captured by ngrep:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="####
T 127.0.0.1:56228 -&gt; 127.0.0.1:4080 [AP] #42
PUT /foo HTTP/1.1.
Host: localhost:4080.
User-Agent: curl/7.74.0.
Accept: */*.
content-type: text/plain.
Content-Length: 5.
.
hello
##
T 127.0.0.1:4080 -&gt; 127.0.0.1:56228 [AP] #44
HTTP/1.1 200 OK.
Server: Werkzeug/2.1.1 Python/3.9.7.
Date: Sat, 16 Apr 2022 00:10:10 GMT.
Content-Type: text/html; charset=utf-8.
Content-Length: 16.
.

##
T 127.0.0.1:4080 -&gt; 127.0.0.1:56228 [AP] #46
simple response

###
T 127.0.0.1:4080 -&gt; 127.0.0.1:56228 [AP] #49
&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.01//EN&quot;
        &quot;http://www.w3.org/TR/html4/strict.dtd&quot;&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html;charset=utf-8&quot;&gt;
        &lt;title&gt;Error response&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h1&gt;Error response&lt;/h1&gt;
        &lt;p&gt;Error code: 400&lt;/p&gt;
        &lt;p&gt;Message: Bad request syntax ('hello').&lt;/p&gt;
        &lt;p&gt;Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.&lt;/p&gt;
    &lt;/body&gt;
&lt;/html&gt;

#^Cexit
50 received, 16 matched"><pre class="notranslate"><code class="notranslate">####
T 127.0.0.1:56228 -&gt; 127.0.0.1:4080 [AP] #42
PUT /foo HTTP/1.1.
Host: localhost:4080.
User-Agent: curl/7.74.0.
Accept: */*.
content-type: text/plain.
Content-Length: 5.
.
hello
##
T 127.0.0.1:4080 -&gt; 127.0.0.1:56228 [AP] #44
HTTP/1.1 200 OK.
Server: Werkzeug/2.1.1 Python/3.9.7.
Date: Sat, 16 Apr 2022 00:10:10 GMT.
Content-Type: text/html; charset=utf-8.
Content-Length: 16.
.

##
T 127.0.0.1:4080 -&gt; 127.0.0.1:56228 [AP] #46
simple response

###
T 127.0.0.1:4080 -&gt; 127.0.0.1:56228 [AP] #49
&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd"&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta http-equiv="Content-Type" content="text/html;charset=utf-8"&gt;
        &lt;title&gt;Error response&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h1&gt;Error response&lt;/h1&gt;
        &lt;p&gt;Error code: 400&lt;/p&gt;
        &lt;p&gt;Message: Bad request syntax ('hello').&lt;/p&gt;
        &lt;p&gt;Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.&lt;/p&gt;
    &lt;/body&gt;
&lt;/html&gt;

#^Cexit
50 received, 16 matched
</code></pre></div>
<p dir="auto">The logging emitted by my <code class="notranslate">flaskdouble</code> app is also curious:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="127.0.0.1 - - [15/Apr/2022 20:13:11] &quot;PUT /foo HTTP/1.1&quot; 200 -
127.0.0.1 - - [15/Apr/2022 20:13:11] code 400, message Bad request syntax ('hello')
127.0.0.1 - - [15/Apr/2022 20:13:11] &quot;None /foo HTTP/0.9&quot; HTTPStatus.BAD_REQUEST -"><pre class="notranslate"><code class="notranslate">127.0.0.1 - - [15/Apr/2022 20:13:11] "PUT /foo HTTP/1.1" 200 -
127.0.0.1 - - [15/Apr/2022 20:13:11] code 400, message Bad request syntax ('hello')
127.0.0.1 - - [15/Apr/2022 20:13:11] "None /foo HTTP/0.9" HTTPStatus.BAD_REQUEST -
</code></pre></div>
<p dir="auto">Expected behaviour:</p>
<ul dir="auto">
<li>the app should write exactly 16 bytes to its client, namely "simple response\n". That second response body, an HTML error document, should not be there.</li>
<li>the app should log exactly one line, namely <code class="notranslate">"PUT /foo HTTP/1.1" 200 -</code></li>
</ul>
<p dir="auto">Environment:</p>
<ul dir="auto">
<li>Python version: 3.9.7</li>
<li>Flask version: 2.1.1-12-g2b8b4781 (in a git working dir)</li>
</ul>
<p dir="auto">My virtualenv is barebones:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="./env/bin/pip freeze      
click==8.1.2
-e git+ssh://git@github.com/pallets/flask.git@2b8b47817a981b97a233420b5214f6ff9e771c35#egg=Flask
importlib-metadata==4.11.3
itsdangerous==2.1.2
Jinja2==3.1.1
MarkupSafe==2.1.1
pkg_resources==0.0.0
Werkzeug==2.1.1
zipp==3.8.0"><pre class="notranslate"><code class="notranslate">./env/bin/pip freeze      
click==8.1.2
-e git+ssh://git@github.com/pallets/flask.git@2b8b47817a981b97a233420b5214f6ff9e771c35#egg=Flask
importlib-metadata==4.11.3
itsdangerous==2.1.2
Jinja2==3.1.1
MarkupSafe==2.1.1
pkg_resources==0.0.0
Werkzeug==2.1.1
zipp==3.8.0
</code></pre></div>
<p dir="auto">With the following example:</p>
<div class="highlight highlight-source-python notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="from flask import Flask

app = Flask(__name__)


@app.route('/', methods=['POST'])
def hello_world():
    return 'Hello World!'


if __name__ == '__main__':
    app.run()"><pre class="notranslate"><span class="pl-k">from</span> <span class="pl-s1">flask</span> <span class="pl-k">import</span> <span class="pl-v">Flask</span>

<span class="pl-s1">app</span> <span class="pl-c1">=</span> <span class="pl-v">Flask</span>(<span class="pl-s1">__name__</span>)


<span class="pl-en">@<span class="pl-s1">app</span>.<span class="pl-en">route</span>(<span class="pl-s">'/'</span>, <span class="pl-s1">methods</span><span class="pl-c1">=</span>[<span class="pl-s">'POST'</span>])</span>
<span class="pl-k">def</span> <span class="pl-en">hello_world</span>():
    <span class="pl-k">return</span> <span class="pl-s">'Hello World!'</span>


<span class="pl-k">if</span> <span class="pl-s1">__name__</span> <span class="pl-c1">==</span> <span class="pl-s">'__main__'</span>:
    <span class="pl-s1">app</span>.<span class="pl-en">run</span>()</pre></div>
<p dir="auto">When you set the request body to <code class="notranslate">{}</code> with Postman or any HTTP clients, the first request will return 200, while the second request will return a 405 error response. The log shows the request method is <code class="notranslate">{}POST</code>:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="&quot;{}POST / HTTP/1.1&quot; 405"><pre class="notranslate"><code class="notranslate">"{}POST / HTTP/1.1" 405
</code></pre></div>
<p dir="auto">Notice the request body became the part of the request method.</p>