1
+ from collections .abc import Mapping
1
2
import json
3
+ from json import JSONDecodeError
2
4
import sys
3
5
import time
4
6
import importlib
9
11
from readme_metrics import ResponseInfoWrapper
10
12
from werkzeug import Request
11
13
12
- from readme_metrics .util import util_exclude_keys , util_filter_keys
13
-
14
14
15
15
class PayloadBuilder :
16
16
"""
@@ -97,56 +97,20 @@ def _build_request_payload(self, request: Request) -> dict:
97
97
Returns:
98
98
dict: Wrapped request payload
99
99
"""
100
- post_data = {}
101
- headers = None
102
-
103
- # Convert EnivronHeaders to a dictionary
104
- headers_dict = dict (request .headers .items ())
105
- if self .denylist :
106
- headers = util_exclude_keys (headers_dict , self .denylist )
107
- elif self .allowlist :
108
- headers = util_filter_keys (headers_dict , self .allowlist )
109
- else :
110
- headers = headers_dict
111
-
112
- if request .content_length is not None and request .content_length > 0 :
113
-
114
- body = request .rm_body .decode ("utf-8" ) or ""
115
-
116
- try :
117
- json_object = json .loads (body )
100
+ headers = self ._redact_dict (request .headers )
101
+ params = parse .parse_qsl (request .query_string .decode ("utf-8" ))
118
102
119
- if self .denylist :
120
- body = util_exclude_keys (json_object , self .denylist )
121
- elif self .allowlist :
122
- body = util_filter_keys (json_object , self .allowlist )
123
-
124
- post_data ["mimeType" ] = "application/json"
125
- post_data ["text" ] = body
126
- except ValueError as e :
127
- post_data ["params" ] = [body ]
128
-
129
- if request .content_type :
130
- post_data ["mimeType" ] = request .content_type
131
- else :
132
- post_data ["mimeType" ] = "text/html"
133
-
134
- hdr_items = []
135
- for k , v in headers .items ():
136
- hdr_items .append ({"name" : k , "value" : v })
137
-
138
- qs_items = []
139
- qs_dict = dict (parse .parse_qsl (request .query_string .decode ("utf-8" )))
140
-
141
- for k , v in qs_dict .items ():
142
- qs_items .append ({"name" : k , "value" : v })
103
+ if request .content_length :
104
+ post_data = self ._process_body (request .rm_body )
105
+ else :
106
+ post_data = {}
143
107
144
108
return {
145
109
"method" : request .method ,
146
110
"url" : request .base_url ,
147
111
"httpVersion" : request .environ ["SERVER_PROTOCOL" ],
148
- "headers" : hdr_items ,
149
- "queryString" : qs_items ,
112
+ "headers" : [{ "name" : k , "value" : v } for ( k , v ) in headers . items ()] ,
113
+ "queryString" : [{ "name" : k , "value" : v } for ( k , v ) in params ] ,
150
114
** post_data ,
151
115
}
152
116
@@ -159,28 +123,10 @@ def _build_response_payload(self, response: ResponseInfoWrapper) -> dict:
159
123
Returns:
160
124
dict: Wrapped response payload
161
125
"""
162
- if self .denylist :
163
- headers = util_exclude_keys (response .headers , self .denylist )
164
- elif self .allowlist :
165
- headers = util_filter_keys (response .headers , self .allowlist )
166
- else :
167
- headers = response .headers
168
-
169
- body = response .body
170
-
171
- try :
172
- json_object = json .loads (body )
173
-
174
- if self .denylist :
175
- body = util_exclude_keys (json_object , self .denylist )
176
- elif self .allowlist :
177
- body = util_filter_keys (json_object , self .allowlist )
178
- except ValueError :
179
- pass
126
+ headers = self ._redact_dict (response .headers )
127
+ body = self ._process_body (response .body ).get ("text" )
180
128
181
- hdr_items = []
182
- for k , v in headers .items ():
183
- hdr_items .append ({"name" : k , "value" : v })
129
+ headers = [{"name" : k , "value" : v } for (k , v ) in headers .items ()]
184
130
185
131
status_string = str (response .status )
186
132
status_code = int (status_string .split (" " )[0 ])
@@ -189,10 +135,74 @@ def _build_response_payload(self, response: ResponseInfoWrapper) -> dict:
189
135
return {
190
136
"status" : status_code ,
191
137
"statusText" : status_text or "" ,
192
- "headers" : hdr_items , # headers.items(),
138
+ "headers" : headers , # headers.items(),
193
139
"content" : {
194
140
"text" : body ,
195
141
"size" : response .content_length ,
196
142
"mimeType" : response .content_type ,
197
143
},
198
144
}
145
+
146
+ # always returns a dict with some of these fields: text, mimeType, params}
147
+ def _process_body (self , body ):
148
+ if isinstance (body , bytes ):
149
+ # Non-unicode bytes cannot be directly serialized as a JSON
150
+ # payload to send to the ReadMe API, so we need to convert this to a
151
+ # unicode string first. But we don't know what encoding it might be
152
+ # using, if any (it could also just be raw bytes, like an image).
153
+ # We're going to assume that if it's possible to decode at all, then
154
+ # it's most likely UTF-8. If we can't decode it, just send an error
155
+ # with the JSON payload.
156
+ try :
157
+ body = body .decode ("utf-8" )
158
+ except UnicodeDecodeError :
159
+ return {"text" : "[ERROR: NOT VALID UTF-8]" }
160
+
161
+ if not isinstance (body , str ):
162
+ # We don't know how to process this body. If it's safe to encode as
163
+ # JSON, return it unchanged; otherwise return an error.
164
+ try :
165
+ json .dumps (body )
166
+ return {"text" : body }
167
+ except TypeError :
168
+ return {"text" : "[ERROR: NOT SERIALIZABLE]" }
169
+
170
+ try :
171
+ body_data = json .loads (body )
172
+ except JSONDecodeError :
173
+ params = parse .parse_qsl (body )
174
+ if params :
175
+ return {
176
+ "text" : body ,
177
+ "mimeType" : "multipart/form-data" ,
178
+ "params" : [{"name" : k , "value" : v } for (k , v ) in params ],
179
+ }
180
+ else :
181
+ return {"text" : body }
182
+
183
+ if (self .denylist or self .allowlist ) and isinstance (body_data , dict ):
184
+ redacted_data = self ._redact_dict (body_data )
185
+ body = json .dumps (redacted_data )
186
+
187
+ return {"text" : body , "mimeType" : "application/json" }
188
+
189
+ def _redact_dict (self , mapping : Mapping ):
190
+ def _redact_value (v ):
191
+ if isinstance (v , str ):
192
+ return f"[REDACTED { len (v )} ]"
193
+ else :
194
+ return "[REDACTED]"
195
+
196
+ # Short-circuit this function if there's no allowlist or denylist
197
+ if not (self .allowlist or self .denylist ):
198
+ return mapping
199
+
200
+ result = dict ()
201
+ for (key , value ) in mapping .items ():
202
+ if self .denylist and key in self .denylist :
203
+ result [key ] = _redact_value (value )
204
+ elif self .allowlist and key not in self .allowlist :
205
+ result [key ] = _redact_value (value )
206
+ else :
207
+ result [key ] = value
208
+ return result
0 commit comments