11import importlib
22import threading
33from datetime import datetime , timedelta
4+ from enum import Enum
45from json import dumps
56from typing import Any , Dict , Generator , Iterable , List , Mapping , Optional , Tuple
67
@@ -153,16 +154,36 @@ def request_handler(request: Request) -> Response:
153154 return r
154155
155156
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 ]]:
157166 """
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 .
159168
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.
162183 """
163184 _ctypes = {'xml' : 'application/samlmetadata+xml;application/xml;text/xml' , 'json' : 'application/json' }
164185
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 ]]:
166187 """ Split a path into a base component and an extension. """
167188 if x is not None :
168189 x = x .strip ()
@@ -178,6 +199,45 @@ def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional
178199
179200 return x , None
180201
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+ """
181241 log .debug (f'Processing request: { request } ' )
182242
183243 if request .matchdict is None :
@@ -215,58 +275,23 @@ def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional
215275 if pfx is None :
216276 raise exc .exception_response (404 )
217277
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
255286
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 )
262288
263289 try :
264- accepter = MediaAccept (accept )
265290 for p in request .registry .plumbings :
266291 state = {
267292 entry : True ,
268293 'headers' : {'Content-Type' : None },
269- 'accept' : accepter ,
294+ 'accept' : accept ,
270295 'url' : request .current_route_url (),
271296 'select' : q ,
272297 'match' : match .lower () if match else match ,
@@ -284,7 +309,7 @@ def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional
284309 response .headers .update (_headers )
285310 ctype = _headers .get ('Content-Type' , None )
286311 if not ctype :
287- r , t = _fmt (r , accepter )
312+ r , t = _fmt (r , accept )
288313 ctype = t
289314
290315 response .text = b2u (r )
0 commit comments