From 9e9cb84724cfb72e7780a7c37c59468e55bb8f0d Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Mon, 27 Nov 2017 07:13:46 +1100 Subject: [PATCH 1/2] Added support for chunked encoded requests when running gunicorn. Fixed #340. In this commit: - when we see a Transfer-Encoding: chunked request, and the server is gunicorn, we set environ wsgi.input_terminated, which is required by Werkzeug in the absence of Content-Lenght, or it will empty the data stream. - for chunked requests to non-gunicorn, return 501 Not Implemented. --- httpbin/core.py | 26 +++++++++++++++++++++++++- test_httpbin.py | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/httpbin/core.py b/httpbin/core.py index 035d1c2b..d4263f85 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -15,7 +15,7 @@ import uuid import argparse -from flask import Flask, Response, request, render_template, redirect, jsonify as flask_jsonify, make_response, url_for +from flask import Flask, Response, request, render_template, redirect, jsonify as flask_jsonify, make_response, url_for, abort from flask_common import Common from six.moves import range as xrange from werkzeug.datastructures import WWWAuthenticate, MultiDict @@ -84,6 +84,30 @@ def jsonify(*args, **kwargs): # ----------- # Middlewares # ----------- +""" +https://github.com/kennethreitz/httpbin/issues/340 +Adds a middleware to provide chunked request encoding support running under +gunicorn only. +Werkzeug required environ 'wsgi.input_terminated' to be set otherwise it +empties the input request stream. +- gunicorn seems to support input_terminated but does not add the environ, + so we add it here. +- flask will hang and does not seem to properly terminate the request, so + we explicityly deny chunked requests. +""" +@app.before_request +def before_request(): + if request.environ.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked': + server = request.environ.get('SERVER_SOFTWARE', '') + if server.lower().startswith('gunicorn/'): + if 'wsgi.input_terminated' in request.environ: + app.logger.warning("environ wsgi.input_terminated already set, keeping: %s" + % request.environ['wsgi.input_terminated']) + else: + request.environ['wsgi.input_terminated'] = 1 + else: + abort(501, "Chunked requests are not supported for server %s" % server) + @app.after_request def set_cors_headers(response): response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*') diff --git a/test_httpbin.py b/test_httpbin.py index 5a0e5554..5a82286f 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -206,6 +206,25 @@ def test_post_file_with_missing_content_type_header(self): ) self.assertEqual(response.status_code, 200) + """ + This is currently a sort of negative-test. + We validate that when running Flask-only server that + Transfer-Encoding: chunked requests are unsupported and + we return 501 Not Implemented + """ + def test_post_chunked(self): + data = '{"animal":"dog"}' + response = self.app.post( + '/post', + content_type='application/json', + headers=[('Transfer-Encoding', 'chunked')], + data=data, + ) + self.assertEqual(response.status_code, 501) + #self.assertEqual(response.status_code, 200) + #self.assertEqual(json.loads(response.data.decode('utf-8'))['data'], '{"animal":"dog"}') + #self.assertEqual(json.loads(response.data.decode('utf-8'))['json'], {"animal": "dog"}) + def test_set_cors_headers_after_request(self): response = self.app.get('/get') self.assertEqual( From ec79fee28a76daf51e550af26c67847272996ae5 Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Fri, 1 Dec 2017 09:06:46 +1100 Subject: [PATCH 2/2] Dropped logger-warning for input_terminated to debug. --- httpbin/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpbin/core.py b/httpbin/core.py index d4263f85..bc9cc3e4 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -101,7 +101,7 @@ def before_request(): server = request.environ.get('SERVER_SOFTWARE', '') if server.lower().startswith('gunicorn/'): if 'wsgi.input_terminated' in request.environ: - app.logger.warning("environ wsgi.input_terminated already set, keeping: %s" + app.logger.debug("environ wsgi.input_terminated already set, keeping: %s" % request.environ['wsgi.input_terminated']) else: request.environ['wsgi.input_terminated'] = 1