getsentry / raven-python

Raven is the legacy Python client for Sentry (getsentry.com) — replaced by sentry-python
https://sentry.io
BSD 3-Clause "New" or "Revised" License
1.68k stars 657 forks source link

Raven fails when trying to capture an unhashable (mutable) exception #1278

Open immerrr opened 5 years ago

immerrr commented 5 years ago

I've run into this recently. After a rather short debugging session I've narrowed the culprit down to these lines:

https://github.com/immerrr/raven-python/blob/master/raven/events.py#L51-L52

The problem is that I'm using a custom exception class implemented over attrs that by default sets __hash__ = None as probably all sane mutable classes should do (some background in python-attrs/attrs#136):

In [11]: import attr
    ...: @attr.s
    ...: class MyException(Exception):
    ...:     f = attr.ib()
    ...: set([MyException('foo')])
    ...: 
    ...: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-eb39bb116374> in <module>()
      3 class MyException(Exception):
      4     f = attr.ib()
----> 5 set([MyException('foo')])

TypeError: unhashable type: 'MyException'

I tried fixing this on attrs side by using frozen= which assures the library that the resulting class will be immutable and it's safe to generate a __hash__ function. But then I realized I'm attaching a dictionary to the exception and dictionaries are mutable themselves, so the result is still unhashable:

In [14]: import attr
    ...: @attr.s(frozen=True)
    ...: class MyException(Exception):
    ...:     f = attr.ib()
    ...: set([MyException({'foo': 'bar'})])
    ...: 
    ...: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-16b4c8ca538e> in <module>()
      3 class MyException(Exception):
      4     f = attr.ib()
----> 5 set([MyException({'foo': 'bar'})])

<attrs generated hash 926d52d2b56cfd446b915894a46e5c7977941b62> in __hash__(self)
      2     return hash((
      3         -172549962914795092,
----> 4         self.f,
      5     ))
TypeError: unhashable type: 'dict'

Interestingly enough, builtin exceptions allow mutable fields:

In [16]: set([Exception({'foo': 'bar'})])
Out[16]: {Exception({'foo': 'bar'})}

But probably only because their hash function depends on id(...) and not on the content.

In [17]: set([Exception({'foo': 'bar'}), Exception({'foo': 'bar'})])
Out[17]: {Exception({'foo': 'bar'}), Exception({'foo': 'bar'})}

In [18]: set([Exception({'foo': 'bar'})]*2)
Out[18]: {Exception({'foo': 'bar'})}

So a really straighforward way to fix this would be to deduplicate exceptions only by their id. A slightly more convoluted one would be to create a EnsureHashable wrapper object that would try calculating the wrapped exception's hash falling back to object.__hash__ implementation. If that's OK I could send in a PR doing either of those two.

Also, it would be nice if raven reported some sort of a generic "Error when capturing exception" error so that sentry could send an alert. Given that my service didn't experience any connectivity problems, I expected raven/sentry duo to notify me about any problems via email. However, in this case it didn't work like that, and I discovered this problem while going through the logs on a completely unrelated matter.