Skip to content

Commit

Permalink
Fixes #12384: Add a three-second timeout for RSS reader widget
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed May 1, 2023
1 parent 261f5e4 commit 9319cff
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 22 deletions.
1 change: 1 addition & 0 deletions docs/release-notes/version-3.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Bug Fixes

* [#12380](https://github.com/netbox-community/netbox/issues/12380) - Allow selecting object change as model under object list widget configuration
* [#12384](https://github.com/netbox-community/netbox/issues/12384) - Add a three-second timeout for RSS reader widget
* [#12395](https://github.com/netbox-community/netbox/issues/12395) - Fix "create & add another" action for objects with custom fields
* [#12396](https://github.com/netbox-community/netbox/issues/12396) - Provider account should not be a required field in REST API serializer
* [#12405](https://github.com/netbox-community/netbox/issues/12405) - Fix filtering for VLAN groups displayed under site view
Expand Down
50 changes: 32 additions & 18 deletions netbox/extras/dashboard/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from urllib.parse import urlencode

import feedparser
import requests
from django import forms
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
Expand Down Expand Up @@ -269,12 +270,9 @@ class ConfigForm(WidgetConfigForm):
)

def render(self, request):
url = self.config['feed_url']
feed = self.get_feed()

return render_to_string(self.template_name, {
'url': url,
'feed': feed,
'url': self.config['feed_url'],
**self.get_feed()
})

@cached_property
Expand All @@ -286,17 +284,33 @@ def cache_key(self):
def get_feed(self):
# Fetch RSS content from cache if available
if feed_content := cache.get(self.cache_key):
feed = feedparser.FeedParserDict(feed_content)
else:
feed = feedparser.parse(
self.config['feed_url'],
request_headers={'User-Agent': f'NetBox/{settings.VERSION}'}
return {
'feed': feedparser.FeedParserDict(feed_content),
}

# Fetch feed content from remote server
try:
response = requests.get(
url=self.config['feed_url'],
headers={'User-Agent': f'NetBox/{settings.VERSION}'},
proxies=settings.HTTP_PROXIES,
timeout=3
)
if not feed.bozo:
# Cap number of entries
max_entries = self.config.get('max_entries')
feed['entries'] = feed['entries'][:max_entries]
# Cache the feed content
cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))

return feed
response.raise_for_status()
except requests.exceptions.RequestException as e:

This comment has been minimized.

Copy link
@candlerb

candlerb May 2, 2023

Contributor

For robustness, catch OSError here? (RequestException is a subclass of this).

>>> requests.exceptions.RequestException.__mro__
(<class 'requests.exceptions.RequestException'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

See #12411 where OP got a http.client.RemoteDisconnected exception (a different subclass of OSError)

>>> http.client.RemoteDisconnected.__mro__
(<class 'http.client.RemoteDisconnected'>, <class 'ConnectionResetError'>, <class 'ConnectionError'>, <class 'OSError'>, <class 'http.client.BadStatusLine'>, <class 'http.client.HTTPException'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

Possibly though, it would be better to catch OSError and Runtime error at a higher level covering all widgets. If a transient error occurs in any widget you don't want it to bring the whole of Netbox down.

This comment has been minimized.

Copy link
@jeremystretch

jeremystretch May 2, 2023

Author Member

See #12411 where OP got a http.client.RemoteDisconnected exception

This is because in v3.5.0 the RSS content was being fetched by feedparser directly. Because feedparser doesn't support passing a timeout, I've adapted the widget to fetch the RSS content using requests instead.

return {
'error': e,
}

# Parse feed content
feed = feedparser.parse(response.content)
if not feed.bozo:
# Cap number of entries
max_entries = self.config.get('max_entries')
feed['entries'] = feed['entries'][:max_entries]
# Cache the feed content
cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))

return {
'feed': feed,
}
10 changes: 6 additions & 4 deletions netbox/templates/extras/dashboard/widgets/rssfeed.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% if not feed.bozo %}
{% if feed and not feed.bozo %}
<div class="list-group list-group-flush">
{% for entry in feed.entries %}
<div class="list-group-item px-1">
Expand All @@ -16,7 +16,9 @@ <h6><a href="{{ entry.link }}">{{ entry.title }}</a></h6>
<span class="text-danger">
<i class="mdi mdi-alert"></i> There was a problem fetching the RSS feed:
</span>
<pre class="m-2">
Response status: {{ feed.status }}
Error: {{ feed.bozo_exception|escape }}</pre>
{% if feed %}
{{ feed.bozo_exception|escape }} (HTTP {{ feed.status }})
{% else %}
{{ error }}
{% endif %}
{% endif %}

0 comments on commit 9319cff

Please sign in to comment.