1
1
import importlib
2
2
import threading
3
3
from datetime import datetime , timedelta
4
+ from enum import Enum
4
5
from json import dumps
5
6
from typing import Any , Dict , Generator , Iterable , List , Mapping , Optional , Tuple
6
7
@@ -153,16 +154,36 @@ def request_handler(request: Request) -> Response:
153
154
return r
154
155
155
156
156
- def process_handler (request : Request ) -> Response :
157
+ class ContentNegPolicy (Enum ):
158
+ extension = 'extension' # current default
159
+ adaptive = 'adaptive'
160
+ header = 'header' # future default
161
+
162
+
163
+ def _process_content_negotiate (
164
+ policy : ContentNegPolicy , alias : str , path : Optional [str ], pfx , request : Request
165
+ ) -> Tuple [MediaAccept , Optional [str ], Optional [str ]]:
157
166
"""
158
- The main request handler for pyFF. Implements API call hooks and content negotiation .
167
+ Determine requested content type, based on policy, Accept request header and path extension .
159
168
160
- :param request: the HTTP request object
161
- :return: the data to send to the client
169
+ content_negotiation_policy is one of three values:
170
+
171
+ 1. extension - current default, inspect the path and if it ends in
172
+ an extension, e.g. .xml or .json, always strip off the extension to
173
+ get the entityID and if no accept header or a wildcard header, then
174
+ use the extension to determine the return Content-Type.
175
+
176
+ 2. adaptive - only if no accept header or if a wildcard, then inspect
177
+ the path and if it ends in an extension strip off the extension to
178
+ get the entityID and use the extension to determine the return
179
+ Content-Type.
180
+
181
+ 3. header - future default, do not inspect the path for an extension and
182
+ use only the Accept header to determine the return Content-Type.
162
183
"""
163
184
_ctypes = {'xml' : 'application/samlmetadata+xml;application/xml;text/xml' , 'json' : 'application/json' }
164
185
165
- def _d (x : Optional [str ], do_split : bool = True ) -> Tuple [Optional [str ], Optional [str ]]:
186
+ def _split_path (x : Optional [str ], do_split : bool = True ) -> Tuple [Optional [str ], Optional [str ]]:
166
187
""" Split a path into a base component and an extension. """
167
188
if x is not None :
168
189
x = x .strip ()
@@ -178,6 +199,45 @@ def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional
178
199
179
200
return x , None
180
201
202
+ # TODO - sometimes the client sends > 1 accept header value with ','.
203
+ accept = str (request .accept ).split (',' )[0 ]
204
+ valid_accept = accept and not ('application/*' in accept or 'text/*' in accept or '*/*' in accept )
205
+
206
+ path_no_extension , extension = _split_path (path , True )
207
+ accept_from_extension = accept
208
+ if extension :
209
+ accept_from_extension = _ctypes .get (extension , accept )
210
+
211
+ if policy == ContentNegPolicy .extension :
212
+ path = path_no_extension
213
+ if not valid_accept :
214
+ accept = accept_from_extension
215
+ elif policy == ContentNegPolicy .adaptive :
216
+ if not valid_accept :
217
+ path = path_no_extension
218
+ accept = accept_from_extension
219
+
220
+ if not accept :
221
+ log .warning ('Could not determine accepted response type' )
222
+ raise exc .exception_response (400 )
223
+
224
+ q : Optional [str ]
225
+ if pfx and path :
226
+ q = f'{{{ pfx } }}{ path } '
227
+ path = f'/{ alias } /{ path } '
228
+ else :
229
+ q = path
230
+
231
+ return MediaAccept (accept ), path , q
232
+
233
+
234
+ def process_handler (request : Request ) -> Response :
235
+ """
236
+ The main request handler for pyFF. Implements API call hooks and content negotiation.
237
+
238
+ :param request: the HTTP request object
239
+ :return: the data to send to the client
240
+ """
181
241
log .debug (f'Processing request: { request } ' )
182
242
183
243
if request .matchdict is None :
@@ -215,58 +275,23 @@ def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional
215
275
if pfx is None :
216
276
raise exc .exception_response (404 )
217
277
218
- # content_negotiation_policy is one of three values:
219
- # 1. extension - current default, inspect the path and if it ends in
220
- # an extension, e.g. .xml or .json, always strip off the extension to
221
- # get the entityID and if no accept header or a wildcard header, then
222
- # use the extension to determine the return Content-Type.
223
- #
224
- # 2. adaptive - only if no accept header or if a wildcard, then inspect
225
- # the path and if it ends in an extension strip off the extension to
226
- # get the entityID and use the extension to determine the return
227
- # Content-Type.
228
- #
229
- # 3. header - future default, do not inspect the path for an extension and
230
- # use only the Accept header to determine the return Content-Type.
231
- policy = config .content_negotiation_policy
232
-
233
- # TODO - sometimes the client sends > 1 accept header value with ','.
234
- accept = str (request .accept ).split (',' )[0 ]
235
- valid_accept = accept and not ('application/*' in accept or 'text/*' in accept or '*/*' in accept )
236
-
237
- new_path : Optional [str ] = path
238
- path_no_extension , extension = _d (new_path , True )
239
- accept_from_extension = accept
240
- if extension :
241
- accept_from_extension = _ctypes .get (extension , accept )
242
-
243
- if policy == 'extension' :
244
- new_path = path_no_extension
245
- if not valid_accept :
246
- accept = accept_from_extension
247
- elif policy == 'adaptive' :
248
- if not valid_accept :
249
- new_path = path_no_extension
250
- accept = accept_from_extension
251
-
252
- if not accept :
253
- log .warning ('Could not determine accepted response type' )
254
- raise exc .exception_response (400 )
278
+ try :
279
+ policy = ContentNegPolicy (config .content_negotiation_policy )
280
+ except ValueError :
281
+ log .debug (
282
+ f'Invalid value for config.content_negotiation_policy: { config .content_negotiation_policy } , '
283
+ f'defaulting to "extension"'
284
+ )
285
+ policy = ContentNegPolicy .extension
255
286
256
- q : Optional [str ]
257
- if pfx and new_path :
258
- q = f'{{{ pfx } }}{ new_path } '
259
- new_path = f'/{ alias } /{ new_path } '
260
- else :
261
- q = new_path
287
+ accept , new_path , q = _process_content_negotiate (policy , alias , path , pfx , request )
262
288
263
289
try :
264
- accepter = MediaAccept (accept )
265
290
for p in request .registry .plumbings :
266
291
state = {
267
292
entry : True ,
268
293
'headers' : {'Content-Type' : None },
269
- 'accept' : accepter ,
294
+ 'accept' : accept ,
270
295
'url' : request .current_route_url (),
271
296
'select' : q ,
272
297
'match' : match .lower () if match else match ,
@@ -284,7 +309,7 @@ def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional
284
309
response .headers .update (_headers )
285
310
ctype = _headers .get ('Content-Type' , None )
286
311
if not ctype :
287
- r , t = _fmt (r , accepter )
312
+ r , t = _fmt (r , accept )
288
313
ctype = t
289
314
290
315
response .text = b2u (r )
0 commit comments