Merge lp:~benji/launchpad/bug-697735-2 into lp:launchpad

Proposed by Benji York
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 13264
Proposed branch: lp:~benji/launchpad/bug-697735-2
Merge into: lp:launchpad
Diff against target: 381 lines (+167/-15)
5 files modified
lib/canonical/launchpad/webapp/errorlog.py (+14/-2)
lib/canonical/launchpad/webapp/interfaces.py (+22/-10)
lib/canonical/launchpad/webapp/menu.py (+4/-1)
lib/canonical/launchpad/webapp/tests/test_errorlog.py (+92/-2)
lib/lp_sitecustomize.py (+35/-0)
To merge this branch: bzr merge lp:~benji/launchpad/bug-697735-2
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+64563@code.launchpad.net

Commit message

[r=gmb][bug=697735] don't do OOPS reports for URL parameter type conversion errors

Description of the change

Bug 697735 is about OOPS reports that are generated when
zope.publisher.browser tries to apply a type conversion (e.g.,
?foo:int=7) and the conversion raises an exception. Generating an error
is fine, but we don't want OOPS reports logged when they occur.

This branch adds a marker interface that can be applied to an exception
that signals the OOPS reporting mechanism that no report should be
logged. Originally I added an underscore-prefixed attribute to
exceptions that shouldn't be recorded, but Gary suggested the
improvement of using a marker interface.

The several tests were added to
lib/canonical/launchpad/webapp/tests/test_errorlog.py cover the new
behavior.

A fair bit of mostly whitespace lint was fixed in
./lib/canonical/launchpad/webapp/interfaces.py as well.

The only suboptimal part of this branch is the need to monkey patch
zope.publisher.browser (see lib/lp_sitecustomize.py) so that we could
wrap the conversion functions with a try/except to mark ValueErrors as
non-oops-report-worthy. Gary and I discussed this aspect of the branch
and felt like it was a reasonable compromise.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Thanks for the lint cleanup along with everything else. r=me.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
2--- lib/canonical/launchpad/webapp/errorlog.py 2011-06-01 19:23:24 +0000
3+++ lib/canonical/launchpad/webapp/errorlog.py 2011-06-15 14:58:50 +0000
4@@ -40,6 +40,7 @@
5 IErrorReport,
6 IErrorReportEvent,
7 IErrorReportRequest,
8+ IUnloggedException,
9 )
10 from canonical.launchpad.webapp.opstats import OpStats
11 from canonical.launchpad.webapp.vhosts import allvhosts
12@@ -372,7 +373,18 @@
13 notify(ErrorReportEvent(entry))
14 return entry
15
16- def _isIgnoredException(self, strtype, request=None):
17+ def _isIgnoredException(self, strtype, request=None, exception=None):
18+ """Should the given exception generate an OOPS or be ignored?
19+
20+ Exceptions will be ignored if they
21+ - are specially tagged as being ignorable by having the marker
22+ interface IUnloggedException
23+ - are of a type included in self._ignored_exceptions, or
24+ - were requested with an off-site REFERRER header and are of a
25+ type included in self._ignored_exceptions_for_offsite_referer
26+ """
27+ if IUnloggedException.providedBy(exception):
28+ return True
29 if strtype in self._ignored_exceptions:
30 return True
31 if strtype in self._ignored_exceptions_for_offsite_referer:
32@@ -409,7 +421,7 @@
33 tb_text = None
34
35 strtype = str(getattr(info[0], '__name__', info[0]))
36- if self._isIgnoredException(strtype, request):
37+ if self._isIgnoredException(strtype, request, info[1]):
38 return
39
40 if not isinstance(info[2], basestring):
41
42=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
43--- lib/canonical/launchpad/webapp/interfaces.py 2011-04-27 16:01:06 +0000
44+++ lib/canonical/launchpad/webapp/interfaces.py 2011-06-15 14:58:50 +0000
45@@ -178,7 +178,8 @@
46 class ILink(ILinkData):
47 """An object that represents a link in a menu.
48
49- The attributes name, url and linked may be set by the menus infrastructure.
50+ The attributes name, url and linked may be set by the menus
51+ infrastructure.
52 """
53
54 name = Attribute("The name of this link in Python data structures.")
55@@ -262,6 +263,7 @@
56 (object_url_requested_for, broken_link_in_chain)
57 )
58
59+
60 # XXX kiko 2007-02-08: this needs reconsideration if we are to make it a truly
61 # generic thing. The problem lies in the fact that half of this (user, login,
62 # time zone, developer) is actually useful inside webapp/, and the other half
63@@ -307,6 +309,7 @@
64 connection blows up.
65 '''
66
67+
68 #
69 # Request
70 #
71@@ -406,6 +409,7 @@
72
73 class CookieAuthLoggedInEvent:
74 implements(ILoggedInEvent)
75+
76 def __init__(self, request, login):
77 self.request = request
78 self.login = login
79@@ -413,6 +417,7 @@
80
81 class CookieAuthPrincipalIdentifiedEvent:
82 implements(IPrincipalIdentifiedEvent)
83+
84 def __init__(self, principal, request, login):
85 self.principal = principal
86 self.request = request
87@@ -421,6 +426,7 @@
88
89 class BasicAuthLoggedInEvent:
90 implements(ILoggedInEvent, IPrincipalIdentifiedEvent)
91+
92 def __init__(self, request, login, principal):
93 # these one from ILoggedInEvent
94 self.login = login
95@@ -436,6 +442,7 @@
96
97 class LoggedOutEvent:
98 implements(ILoggedOutEvent)
99+
100 def __init__(self, request):
101 self.request = request
102
103@@ -527,6 +534,7 @@
104 you're using right now.
105 """)
106
107+
108 class AccessLevel(DBEnumeratedType):
109 """The level of access any given principal has."""
110 use_template(OAuthPermission, exclude='UNAUTHORIZED')
111@@ -553,10 +561,10 @@
112
113 class BrowserNotificationLevel:
114 """Matches the standard logging levels."""
115- DEBUG = logging.DEBUG # A debugging message
116- INFO = logging.INFO # simple confirmation of a change
117- WARNING = logging.WARNING # action will not be successful unless you ...
118- ERROR = logging.ERROR # the previous action did not succeed, and why
119+ DEBUG = logging.DEBUG # debugging message
120+ INFO = logging.INFO # simple confirmation of a change
121+ WARNING = logging.WARNING # action will not be successful unless you ...
122+ ERROR = logging.ERROR # the previous action did not succeed, and why
123
124 ALL_LEVELS = (DEBUG, INFO, WARNING, ERROR)
125
126@@ -646,6 +654,10 @@
127 """
128
129
130+class IUnloggedException(Interface):
131+ """An exception that should not be logged in an OOPS report (marker)."""
132+
133+
134 class IErrorReportEvent(IObjectEvent):
135 """A new error report has been created."""
136
137@@ -673,6 +685,7 @@
138 description=u"""an identifier for the exception, or None if no
139 exception has occurred""")
140
141+
142 #
143 # Batch Navigation
144 #
145@@ -720,12 +733,12 @@
146 # Database policies
147 #
148
149-MAIN_STORE = 'main' # The main database.
150+MAIN_STORE = 'main' # The main database.
151 ALL_STORES = frozenset([MAIN_STORE])
152
153-DEFAULT_FLAVOR = 'default' # Default flavor for current state.
154-MASTER_FLAVOR = 'master' # The master database.
155-SLAVE_FLAVOR = 'slave' # A slave database.
156+DEFAULT_FLAVOR = 'default' # Default flavor for current state.
157+MASTER_FLAVOR = 'master' # The master database.
158+SLAVE_FLAVOR = 'slave' # A slave database.
159
160
161 class IDatabasePolicy(Interface):
162@@ -856,7 +869,6 @@
163
164 request = Attribute("The request the event is about")
165
166-
167 class StartRequestEvent:
168 """An event fired once at the start of requests.
169
170
171=== modified file 'lib/canonical/launchpad/webapp/menu.py'
172--- lib/canonical/launchpad/webapp/menu.py 2011-03-23 16:28:51 +0000
173+++ lib/canonical/launchpad/webapp/menu.py 2011-06-15 14:58:50 +0000
174@@ -39,7 +39,6 @@
175 removeSecurityProxy,
176 )
177
178-from canonical.launchpad.webapp.authorization import check_permission
179 from canonical.launchpad.webapp.interfaces import (
180 IApplicationMenu,
181 IContextMenu,
182@@ -529,6 +528,10 @@
183 """
184 permission = self.permission
185
186+ # This is imported here to forestall an import-time config read that
187+ # wreaks havoc.
188+ from canonical.launchpad.webapp.authorization import check_permission
189+
190 def enable_if_allowed(self):
191 link = func(self)
192 if not check_permission(permission, self.context):
193
194=== modified file 'lib/canonical/launchpad/webapp/tests/test_errorlog.py'
195--- lib/canonical/launchpad/webapp/tests/test_errorlog.py 2011-06-01 19:35:22 +0000
196+++ lib/canonical/launchpad/webapp/tests/test_errorlog.py 2011-06-15 14:58:50 +0000
197@@ -41,7 +41,10 @@
198 OopsLoggingHandler,
199 ScriptRequest,
200 )
201-from canonical.launchpad.webapp.interfaces import NoReferrerError
202+from canonical.launchpad.webapp.interfaces import (
203+ IUnloggedException,
204+ NoReferrerError,
205+ )
206 from canonical.testing import reset_logging
207 from lp.app.errors import (
208 GoneError,
209@@ -50,6 +53,7 @@
210 from lp.services.log.uniquefileallocator import UniqueFileAllocator
211 from lp.services.osutils import remove_tree
212 from lp.testing import TestCase
213+from lp_sitecustomize import customize_get_converter
214
215
216 UTC = pytz.timezone('UTC')
217@@ -967,7 +971,7 @@
218 self.assertIs(None, self.error_utility.getLastOopsReport())
219
220
221-class Test404Oops(testtools.TestCase):
222+class TestOopsIgnoring(testtools.TestCase):
223
224 def test_offsite_404_ignored(self):
225 # A request originating from another site that generates a NotFound
226@@ -990,6 +994,92 @@
227 request = dict()
228 self.assertTrue(utility._isIgnoredException('NotFound', request))
229
230+ def test_marked_exception_is_ignored(self):
231+ # If an exception has been marked as ignorable, then it is ignored.
232+ utility = ErrorReportingUtility()
233+ exception = Exception()
234+ directlyProvides(exception, IUnloggedException)
235+ self.assertTrue(
236+ utility._isIgnoredException('RuntimeError', exception=exception))
237+
238+ def test_unmarked_exception_generates_oops(self):
239+ # If an exception has not been marked as ignorable, then it is not.
240+ utility = ErrorReportingUtility()
241+ exception = Exception()
242+ self.assertFalse(
243+ utility._isIgnoredException('RuntimeError', exception=exception))
244+
245+
246+class TestWrappedParameterConverter(testtools.TestCase):
247+ """Make sure URL parameter type conversions don't generate OOPS reports"""
248+
249+ def test_return_value_untouched(self):
250+ # When a converter succeeds, its return value is passed through the
251+ # wrapper untouched.
252+
253+ class FauxZopePublisherBrowserModule:
254+ def get_converter(self, type_):
255+ def the_converter(value):
256+ return 'converted %r to %s' % (value, type_)
257+ return the_converter
258+
259+ module = FauxZopePublisherBrowserModule()
260+ customize_get_converter(module)
261+ converter = module.get_converter('int')
262+ self.assertEqual("converted '42' to int", converter('42'))
263+
264+ def test_value_errors_marked(self):
265+ # When a ValueError is raised by the wrapped converter, the exception
266+ # is marked with IUnloggedException so the OOPS machinery knows that a
267+ # report should not be logged.
268+
269+ class FauxZopePublisherBrowserModule:
270+ def get_converter(self, type_):
271+ def the_converter(value):
272+ raise ValueError
273+ return the_converter
274+
275+ module = FauxZopePublisherBrowserModule()
276+ customize_get_converter(module)
277+ converter = module.get_converter('int')
278+ try:
279+ converter(42)
280+ except ValueError, e:
281+ self.assertTrue(IUnloggedException.providedBy(e))
282+
283+ def test_other_errors_not_marked(self):
284+ # When an exception other than ValueError is raised by the wrapped
285+ # converter, the exception is not marked with IUnloggedException an
286+ # OOPS report will be created.
287+
288+ class FauxZopePublisherBrowserModule:
289+ def get_converter(self, type_):
290+ def the_converter(value):
291+ raise RuntimeError
292+ return the_converter
293+
294+ module = FauxZopePublisherBrowserModule()
295+ customize_get_converter(module)
296+ converter = module.get_converter('int')
297+ try:
298+ converter(42)
299+ except RuntimeError, e:
300+ self.assertFalse(IUnloggedException.providedBy(e))
301+
302+ def test_none_is_not_wrapped(self):
303+ # The get_converter function that we're wrapping can return None, in
304+ # that case there's no function for us to wrap and we just return None
305+ # as well.
306+
307+ class FauxZopePublisherBrowserModule:
308+ def get_converter(self, type_):
309+ return None
310+
311+ module = FauxZopePublisherBrowserModule()
312+ customize_get_converter(module)
313+ converter = module.get_converter('int')
314+ self.assertTrue(converter is None)
315+
316
317 def test_suite():
318 return unittest.TestLoader().loadTestsFromName(__name__)
319
320=== modified file 'lib/lp_sitecustomize.py'
321--- lib/lp_sitecustomize.py 2011-04-05 12:41:25 +0000
322+++ lib/lp_sitecustomize.py 2011-06-15 14:58:50 +0000
323@@ -16,12 +16,15 @@
324 )
325
326 from bzrlib.branch import Branch
327+from canonical.launchpad.webapp.interfaces import IUnloggedException
328 from lp.services.log import loglevels
329 from lp.services.log.logger import LaunchpadLogger
330 from lp.services.log.mappingfilter import MappingFilter
331 from lp.services.log.nullhandler import NullHandler
332 from lp.services.mime import customizeMimetypes
333+from zope.interface import alsoProvides
334 from zope.security import checker
335+import zope.publisher.browser
336
337
338 def add_custom_loglevels():
339@@ -136,6 +139,37 @@
340 silence_transaction_logger()
341
342
343+def customize_get_converter(zope_publisher_browser=zope.publisher.browser):
344+ """URL parameter conversion errors shouldn't generate an OOPS report.
345+
346+ This injects (monkey patches) our wrapper around get_converter so improper
347+ use of parameter type converters (like http://...?foo=bar:int) won't
348+ generate OOPS reports.
349+ """
350+ original_get_converter = zope_publisher_browser.get_converter
351+
352+ def get_converter(*args, **kws):
353+ """Get a type converter but turn off OOPS reporting if it fails."""
354+ converter = original_get_converter(*args, **kws)
355+
356+ def wrapped_converter(v):
357+ try:
358+ return converter(v)
359+ except ValueError, e:
360+ # Mark the exception as not being OOPS-worthy.
361+ alsoProvides(e, IUnloggedException)
362+ raise
363+
364+ # The converter can be None, in which case wrapping it makes no sense,
365+ # otherwise it is a function which we wrap.
366+ if converter is None:
367+ return None
368+ else:
369+ return wrapped_converter
370+
371+ zope_publisher_browser.get_converter = get_converter
372+
373+
374 def main(instance_name):
375 # This is called by our custom buildout-generated sitecustomize.py
376 # in parts/scripts/sitecustomize.py. The instance name is sent to
377@@ -161,3 +195,4 @@
378 checker.BasicTypes[grouper] = checker._iteratorChecker
379 silence_warnings()
380 customize_logger()
381+ customize_get_converter()