Skip to content

Commit c8a32df

Browse files
remove flask streamed response from 1.5 (#4208) (#4217)
Revert #3826 as an immediate mitigation for #4215 (cherry picked from commit 93e4595) Co-authored-by: Munir Abdinur <[email protected]>
1 parent 1ec50e1 commit c8a32df

15 files changed

+458
-684
lines changed

ddtrace/contrib/flask/patch.py

Lines changed: 108 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from .. import trace_utils
2323
from ...constants import ANALYTICS_SAMPLE_RATE_KEY
2424
from ...constants import SPAN_MEASURED_KEY
25-
from ...contrib.wsgi.wsgi import _DDWSGIMiddlewareBase
2625
from ...ext import SpanTypes
2726
from ...internal.compat import maybe_stringify
2827
from ...internal.logger import get_logger
@@ -90,103 +89,6 @@ class RequestWithJson(werkzeug.Request, JSONMixin):
9089
flask_version = parse_version(flask_version_str)
9190

9291

93-
class _FlaskWSGIMiddleware(_DDWSGIMiddlewareBase):
94-
_request_span_name = "flask.request"
95-
_application_span_name = "flask.application"
96-
_response_span_name = "flask.response"
97-
98-
def _traced_start_response(self, start_response, span, status_code, headers, exc_info=None):
99-
code, _, _ = status_code.partition(" ")
100-
# If values are accessible, set the resource as `<method> <path>` and add other request tags
101-
_set_request_tags(span)
102-
103-
# Override root span resource name to be `<method> 404` for 404 requests
104-
# DEV: We do this because we want to make it easier to see all unknown requests together
105-
# Also, we do this to reduce the cardinality on unknown urls
106-
# DEV: If we have an endpoint or url rule tag, then we don't need to do this,
107-
# we still want `GET /product/<int:product_id>` grouped together,
108-
# even if it is a 404
109-
if not span.get_tag(FLASK_ENDPOINT) and not span.get_tag(FLASK_URL_RULE):
110-
span.resource = u" ".join((flask.request.method, code))
111-
112-
trace_utils.set_http_meta(span, config.flask, status_code=code, response_headers=headers)
113-
114-
return start_response(status_code, headers)
115-
116-
def _request_span_modifier(self, span, environ):
117-
# Create a werkzeug request from the `environ` to make interacting with it easier
118-
# DEV: This executes before a request context is created
119-
request = _RequestType(environ)
120-
121-
# Default resource is method and path:
122-
# GET /
123-
# POST /save
124-
# We will override this below in `traced_dispatch_request` when we have a `
125-
# RequestContext` and possibly a url rule
126-
span.resource = u" ".join((request.method, request.path))
127-
128-
span.set_tag(SPAN_MEASURED_KEY)
129-
# set analytics sample rate with global config enabled
130-
sample_rate = config.flask.get_analytics_sample_rate(use_global_config=True)
131-
if sample_rate is not None:
132-
span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)
133-
134-
span._set_str_tag(FLASK_VERSION, flask_version_str)
135-
136-
req_body = None
137-
if config._appsec_enabled and request.method in _BODY_METHODS:
138-
content_type = request.content_type
139-
wsgi_input = environ.get("wsgi.input", "")
140-
141-
# Copy wsgi input if not seekable
142-
try:
143-
seekable = wsgi_input.seekable()
144-
except AttributeError:
145-
seekable = False
146-
if not seekable:
147-
body = wsgi_input.read()
148-
environ["wsgi.input"] = BytesIO(body)
149-
150-
try:
151-
if content_type == "application/json":
152-
if _HAS_JSON_MIXIN and hasattr(request, "json"):
153-
req_body = request.json
154-
else:
155-
req_body = json.loads(request.data.decode("UTF-8"))
156-
elif content_type in ("application/xml", "text/xml"):
157-
req_body = xmltodict.parse(request.get_data())
158-
elif hasattr(request, "values"):
159-
req_body = request.values.to_dict()
160-
elif hasattr(request, "args"):
161-
req_body = request.args.to_dict()
162-
elif hasattr(request, "form"):
163-
req_body = request.form.to_dict()
164-
else:
165-
req_body = request.get_data()
166-
except (AttributeError, RuntimeError, TypeError, BadRequest, ValueError, JSONDecodeError):
167-
log.warning("Failed to parse werkzeug request body", exc_info=True)
168-
finally:
169-
# Reset wsgi input to the beginning
170-
if seekable:
171-
wsgi_input.seek(0)
172-
else:
173-
environ["wsgi.input"] = BytesIO(body)
174-
175-
trace_utils.set_http_meta(
176-
span,
177-
config.flask,
178-
method=request.method,
179-
url=request.base_url,
180-
raw_uri=request.url,
181-
query=request.query_string,
182-
parsed_query=request.args,
183-
request_headers=request.headers,
184-
request_cookies=request.cookies,
185-
request_body=req_body,
186-
peer_ip=request.remote_addr,
187-
)
188-
189-
19092
def patch():
19193
"""
19294
Patch `flask` module for tracing
@@ -387,6 +289,32 @@ def unpatch():
387289
_u(obj, prop)
388290

389291

292+
# Wrap the `start_response` handler to extract response code
293+
# DEV: We tried using `Flask.finalize_request`, which seemed to work, but gave us hell during tests
294+
# DEV: The downside to using `start_response` is we do not have a `Flask.Response` object here,
295+
# only `status_code`, and `headers` to work with
296+
# On the bright side, this works in all versions of Flask (or any WSGI app actually)
297+
def _wrap_start_response(func, span, request):
298+
def traced_start_response(status_code, headers):
299+
code, _, _ = status_code.partition(" ")
300+
# If values are accessible, set the resource as `<method> <path>` and add other request tags
301+
_set_request_tags(span)
302+
303+
# Override root span resource name to be `<method> 404` for 404 requests
304+
# DEV: We do this because we want to make it easier to see all unknown requests together
305+
# Also, we do this to reduce the cardinality on unknown urls
306+
# DEV: If we have an endpoint or url rule tag, then we don't need to do this,
307+
# we still want `GET /product/<int:product_id>` grouped together,
308+
# even if it is a 404
309+
if not span.get_tag(FLASK_ENDPOINT) and not span.get_tag(FLASK_URL_RULE):
310+
span.resource = u" ".join((request.method, code))
311+
312+
trace_utils.set_http_meta(span, config.flask, status_code=code, response_headers=headers)
313+
return func(status_code, headers)
314+
315+
return traced_start_response
316+
317+
390318
@with_instance_pin
391319
def traced_wsgi_app(pin, wrapped, instance, args, kwargs):
392320
"""
@@ -397,8 +325,88 @@ def traced_wsgi_app(pin, wrapped, instance, args, kwargs):
397325
# DEV: This is safe before this is the args for a WSGI handler
398326
# https://www.python.org/dev/peps/pep-3333/
399327
environ, start_response = args
400-
middleware = _FlaskWSGIMiddleware(wrapped, pin.tracer, config.flask, pin)
401-
return middleware(environ, start_response)
328+
329+
# Create a werkzeug request from the `environ` to make interacting with it easier
330+
# DEV: This executes before a request context is created
331+
request = _RequestType(environ)
332+
333+
# Configure distributed tracing
334+
trace_utils.activate_distributed_headers(pin.tracer, int_config=config.flask, request_headers=request.headers)
335+
336+
# Default resource is method and path:
337+
# GET /
338+
# POST /save
339+
# We will override this below in `traced_dispatch_request` when we have a `RequestContext` and possibly a url rule
340+
resource = u" ".join((request.method, request.path))
341+
with pin.tracer.trace(
342+
"flask.request",
343+
service=trace_utils.int_service(pin, config.flask),
344+
resource=resource,
345+
span_type=SpanTypes.WEB,
346+
) as span:
347+
span.set_tag(SPAN_MEASURED_KEY)
348+
# set analytics sample rate with global config enabled
349+
sample_rate = config.flask.get_analytics_sample_rate(use_global_config=True)
350+
if sample_rate is not None:
351+
span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)
352+
353+
span._set_str_tag(FLASK_VERSION, flask_version_str)
354+
start_response = _wrap_start_response(start_response, span, request)
355+
356+
req_body = None
357+
if config._appsec_enabled and request.method in _BODY_METHODS:
358+
content_type = request.content_type
359+
wsgi_input = environ.get("wsgi.input", "")
360+
361+
# Copy wsgi input if not seekable
362+
try:
363+
seekable = wsgi_input.seekable()
364+
except AttributeError:
365+
seekable = False
366+
if not seekable:
367+
body = wsgi_input.read()
368+
environ["wsgi.input"] = BytesIO(body)
369+
370+
try:
371+
if content_type == "application/json":
372+
if _HAS_JSON_MIXIN and hasattr(request, "json"):
373+
req_body = request.json
374+
else:
375+
req_body = json.loads(request.data.decode("UTF-8"))
376+
elif content_type in ("application/xml", "text/xml"):
377+
req_body = xmltodict.parse(request.get_data())
378+
elif hasattr(request, "values"):
379+
req_body = request.values.to_dict()
380+
elif hasattr(request, "args"):
381+
req_body = request.args.to_dict()
382+
elif hasattr(request, "form"):
383+
req_body = request.form.to_dict()
384+
else:
385+
req_body = request.get_data()
386+
except (AttributeError, RuntimeError, TypeError, BadRequest, ValueError, JSONDecodeError):
387+
log.warning("Failed to parse werkzeug request body", exc_info=True)
388+
finally:
389+
# Reset wsgi input to the beginning
390+
if seekable:
391+
wsgi_input.seek(0)
392+
else:
393+
environ["wsgi.input"] = BytesIO(body)
394+
395+
trace_utils.set_http_meta(
396+
span,
397+
config.flask,
398+
method=request.method,
399+
url=request.base_url,
400+
raw_uri=request.url,
401+
query=request.query_string,
402+
parsed_query=request.args,
403+
request_headers=request.headers,
404+
request_cookies=request.cookies,
405+
request_body=req_body,
406+
peer_ip=request.remote_addr,
407+
)
408+
409+
return wrapped(environ, start_response)
402410

403411

404412
def traced_blueprint_register(wrapped, instance, args, kwargs):

releasenotes/notes/flask-support-response-streamig-d3aafc8fa1e3605d.yaml

Lines changed: 0 additions & 4 deletions
This file was deleted.

tests/contrib/flask/test_errorhandler.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ def test_default_404_handler(self):
1515
# Make our 404 request
1616
res = self.client.get("/unknown")
1717
self.assertEqual(res.status_code, 404)
18-
# Read response data from the test client to close flask.request and flask.response spans
19-
self.assertIsNotNone(res.data)
2018

2119
spans = self.get_spans()
2220

@@ -59,8 +57,6 @@ def endpoint_500():
5957
# Make our 500 request
6058
res = self.client.get("/500")
6159
self.assertEqual(res.status_code, 500)
62-
# Read response data from the test client to close flask.request and flask.response spans
63-
self.assertIsNotNone(res.data)
6460

6561
spans = self.get_spans()
6662

@@ -121,8 +117,7 @@ def endpoint_500():
121117
# Make our 500 request
122118
res = self.client.get("/500")
123119
self.assertEqual(res.status_code, 200)
124-
# Read response data from the test client to close flask.request and flask.response spans
125-
self.assertIsNotNone(res.data)
120+
self.assertEqual(res.data, b"whoops")
126121

127122
spans = self.get_spans()
128123

@@ -188,8 +183,6 @@ def endpoint_error():
188183
# Make our 500 request
189184
res = self.client.get("/error")
190185
self.assertEqual(res.status_code, 500)
191-
# Read response data from the test client to close flask.request and flask.response spans
192-
self.assertIsNotNone(res.data)
193186

194187
spans = self.get_spans()
195188

tests/contrib/flask/test_flask_appsec.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ def dynamic_url(item):
6767
self._aux_appsec_prepare_tracer()
6868
resp = self.client.get("/params/w00tw00t.at.isc.sans.dfind")
6969
assert resp.status_code == 200
70-
# Read response data from the test client to close flask.request and flask.response spans
71-
assert resp.data is not None
7270

7371
root_span = self.pop_spans()[0]
7472

@@ -94,8 +92,6 @@ def test_flask_cookie_sql_injection(self):
9492
self._aux_appsec_prepare_tracer()
9593
self.client.set_cookie("localhost", "attack", "1' or '1' = '1'")
9694
resp = self.client.get("/")
97-
# Read response data from the test client to close flask.request and flask.response spans
98-
assert resp.data is not None
9995
assert resp.status_code == 404
10096
root_span = self.pop_spans()[0]
10197

0 commit comments

Comments
 (0)