Skip to content
This repository

Accept header normalization #620

Merged
merged 3 commits into from over 1 year ago

3 participants

lmctv Chris McDonough Michael Merickel
lmctv
lmctv commented June 08, 2012

According to RFC 2616, the mimetype in the accept header should be matched in a case insensitive way.

While fixing the root case insensitivity in webob (Pylons/webob#56), I noticed a glitch in view registration: if you run pyramid_view_accept.py with an unpatched pyramid, both the view callables get registered; which one will be called depends on the client-supplied Accept header, and the webob available version...

$ python pyramid_view_accept.py
Route: service, Path: /, Callable: service_HTML, Accept: text/HTML
Route: service, Path: /, Callable: service_html, Accept: text/html
Listening on port 6543
Accept: text/html
localhost.localdomain - - [08/Jun/2012 18:05:41] "GET / HTTP/1.1" 200 92
Accept: text/HTML
localhost.localdomain - - [08/Jun/2012 18:10:39] "GET / HTTP/1.0" 200 92

With the proposed patches, the configurator would notice the attempted shadowing and correctly raises a ConfigurationConflictError:

$ python pyramid_view_accept.py
Traceback (most recent call last):
  File "pyramid_view_accept.py", line 38, in <module>
    app = config.make_wsgi_app()
  File "/home/lorenzo/pyramid/pyramid/config/__init__.py", line 921, in make_wsgi_app
    self.commit()
  File "/home/lorenzo/pyramid/pyramid/config/__init__.py", line 594, in commit
    self.action_state.execute_actions(introspector=self.introspector)
  File "/home/lorenzo/pyramid/pyramid/config/__init__.py", line 1030, in execute_actions
    for action in resolveConflicts(self.actions):
  File "/home/lorenzo/pyramid/pyramid/config/__init__.py", line 1136, in resolveConflicts
    raise ConfigurationConflictError(conflicts)
pyramid.exceptions.ConfigurationConflictError: Conflicting configuration actions
  For: ('view', None, '', None, <InterfaceClass pyramid.interfaces.IView>, None, None, ('GET', 'HEAD'), 'service', None, False, 'text/html', None, None, None)
    Line 12 of file pyramid_view_accept.py:
        accept="text/HTML")
    Line 18 of file pyramid_view_accept.py:
        accept="text/html")

pyramid_view_accept.py:

#!python
# encoding: utf-8

from pyramid.config import Configurator
from wsgiref.simple_server import make_server
from pyramid.view import view_config
from pyramid.response import Response

import sys

@view_config(route_name='service', request_method='GET',
             accept="text/HTML")
def service_HTML(request):
    return Response('<html><head><title>HTML SERVICE</title></head><body><p>route_name=service</p></body></html>\n')

@view_config(route_name='service', request_method='GET',
             accept="text/html")
def service_html(request):
    return Response('<html><head><title>html service</title></head><body><p>route_name=service</p></body></html>\n')


if __name__ == '__main__':
    # configuration settings
    if len(sys.argv) > 1:
        httpport = int(sys.argv[1])
    else:
        httpport = 6543
    settings = {}
    settings['reload_all'] = True
    settings['debug_all'] = True
    # configuration setup
    config = Configurator(settings=settings)
    config.add_route(name='service', pattern='/')
    config.scan()
    # serve app
    app = config.make_wsgi_app()
    # print view mappings
    rt_map = config.get_routes_mapper()
    views = config.introspector.get_category('views')
    for view in views:
        _callable = view['introspectable']['callable'].__name__
        _accept = view['introspectable']['accept']
        _route_nm = view['introspectable']['route_name']
        _route = rt_map.get_route(_route_nm)
        if _route:
            print 'Route: %s, Path: %s, Callable: %s, Accept: %s' % (_route.name, _route.path, _callable, _accept)
    server = make_server('', httpport, app)
    print 'Listening on port %i' % httpport
    server.serve_forever()
added some commits June 08, 2012
lmctv RFC 2616 sec. 3.7 case insensitive match test
Since "... The type, subtype, and parameter attribute names are case-
insensitive..." the type and subtype in the accept parameter in add_view
should be treated in a case insensitive way.
34f00ae
lmctv Lowercase the accept parameter in add_view
Fix the RFC 2616 sec. 3.7 compliance by storing a canonical cased
version of the parameter.
ed9663c
Chris McDonough
Owner
mcdonc commented June 13, 2012

This looks good. Do you think you can also put your name in CONTRIBUTORS.txt (in the Pyramid root project dir)?

lmctv Accept the Contributor Agreement.
as suggested by "Contributing Source Code and Documentation" document.
ce5b5e4
Michael Merickel mmerickel merged commit ce5b5e4 into from August 23, 2012
Michael Merickel mmerickel closed this August 23, 2012
Michael Merickel
Owner

thanks, I fixed a py3 compatibility and updated changes.txt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 3 unique commits by 1 author.

Jun 08, 2012
lmctv RFC 2616 sec. 3.7 case insensitive match test
Since "... The type, subtype, and parameter attribute names are case-
insensitive..." the type and subtype in the accept parameter in add_view
should be treated in a case insensitive way.
34f00ae
lmctv Lowercase the accept parameter in add_view
Fix the RFC 2616 sec. 3.7 compliance by storing a canonical cased
version of the parameter.
ed9663c
Jun 13, 2012
lmctv Accept the Contributor Agreement.
as suggested by "Contributing Source Code and Documentation" document.
ce5b5e4
This page is out of date. Refresh to see the latest.
2  CONTRIBUTORS.txt
@@ -172,3 +172,5 @@ Contributors
172 172
 - Wayne Witzel III, 2012/03/27
173 173
 
174 174
 - Marin Rukavina, 2012/05/03
  175
+
  176
+- Lorenzo M. Catucci, 2012/06/08
3  pyramid/config/views.py
@@ -1001,6 +1001,9 @@ def view(context, request):
1001 1001
                 # GET implies HEAD too
1002 1002
                 request_method = as_sorted_tuple(request_method + ('HEAD',))
1003 1003
 
  1004
+        if accept is not None:
  1005
+            accept = accept.lower()
  1006
+
1004 1007
         order, predicates, phash = make_predicates(xhr=xhr,
1005 1008
             request_method=request_method, path_info=path_info,
1006 1009
             request_param=request_param, header=header, accept=accept,
20  pyramid/tests/test_config/test_views.py
@@ -645,6 +645,26 @@ def view2(context, request):
645 645
         request.accept = DummyAccept('text/html', 'text/html')
646 646
         self.assertEqual(wrapper(None, request), 'OK2')
647 647
 
  648
+    def test_add_view_mixed_case_replaces_existing_view(self):
  649
+        from pyramid.renderers import null_renderer
  650
+        def view(context, request): return 'OK'
  651
+        def view2(context, request): return 'OK2'
  652
+        def view3(context, request): return 'OK3'
  653
+        def get_val(obj, key):
  654
+            return obj.get(key)
  655
+        config = self._makeOne(autocommit=True)
  656
+        config.add_view(view=view, renderer=null_renderer)
  657
+        config.add_view(view=view2, accept='text/html', renderer=null_renderer)
  658
+        config.add_view(view=view3, accept='text/HTML', renderer=null_renderer)
  659
+        wrapper = self._getViewCallable(config)
  660
+        self.assertTrue(IMultiView.providedBy(wrapper))
  661
+        self.assertEqual(len(wrapper.media_views.items()),1)
  662
+        self.assertFalse(wrapper.media_views.has_key('text/HTML'))
  663
+        self.assertEqual(wrapper(None, None), 'OK')
  664
+        request = DummyRequest()
  665
+        request.accept = DummyAccept('text/html', 'text/html')
  666
+        self.assertEqual(wrapper(None, request), 'OK3')
  667
+
648 668
     def test_add_views_with_accept_multiview_replaces_existing(self):
649 669
         from pyramid.renderers import null_renderer
650 670
         def view(context, request): return 'OK'
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.