Skip to content

Commit 4e4de10

Browse files
committed
refactor content negotiation out of process_handler
1 parent 052c556 commit 4e4de10

File tree

1 file changed

+76
-51
lines changed

1 file changed

+76
-51
lines changed

src/pyff/api.py

Lines changed: 76 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import importlib
22
import threading
33
from datetime import datetime, timedelta
4+
from enum import Enum
45
from json import dumps
56
from 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

Comments
 (0)