Repository: plotly/dash

Issue:
requests for missing favicon.ico cause dash's index route to be served
I just noticed that when a favicon is not wired up, the automatic request for `/favicon.ico` by the browser results in the dash server returning its index route. This seems to be a consequence of there being a catchall redirecting all URLs that are not matched to the index route.

While probably not too harmful, this seems undesirable. One solution could be to include a default favicon, but perhaps it is also worth establishing whether the catchall behaviour is desirable?


Discussion:
I like the idea of having a default favicon, maybe the Dash logo? Or we can just add a default route for `favicon.ico` in Dash that returns nothing when there is no `favicon.ico` in the assets directory
Yeah, I like the idea of a built-in Dash favicon too.
I think including a default dash `favicon.ico` is better than a 404.
Here are some favicons we could use
[dash-favicons.zip](https://github.com/plotly/dash/files/2417309/dash-favicons.zip)

I will add the `favicon.ico` in the zip as the default favicon.

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -216,6 +216,9 @@ def add_url(name, view_func, methods=('GET',)):
             '{}<path:path>'.format(self.config['routes_pathname_prefix']),
             self.index)
 
+        add_url('{}_favicon.ico'.format(self.config['routes_pathname_prefix']),
+                self._serve_default_favicon)
+
         self.server.before_first_request(self._setup_server)
 
         self._layout = None
@@ -461,11 +464,22 @@ def index(self, *args, **kwargs):  # pylint: disable=unused-argument
         config = self._generate_config_html()
         metas = self._generate_meta_html()
         title = getattr(self, 'title', 'Dash')
+
         if self._favicon:
-            favicon = '<link rel="icon" type="image/x-icon" href="{}">'.format(
-                self.get_asset_url(self._favicon))
+            favicon_mod_time = os.path.getmtime(
+                os.path.join(self._assets_folder, self._favicon))
+            favicon_url = self.get_asset_url(self._favicon) + '?m={}'.format(
+                favicon_mod_time
+            )
         else:
-            favicon = ''
+            favicon_url = '{}_favicon.ico'.format(
+                self.config.requests_pathname_prefix)
+
+        favicon = _format_tag('link', {
+            'rel': 'icon',
+            'type': 'image/x-icon',
+            'href': favicon_url
+        }, opened=True)
 
         index = self.interpolate_index(
             metas=metas, title=title, css=css, config=config,
@@ -975,6 +989,15 @@ def add_resource(p, filepath):
     def _invalid_resources_handler(self, err):
         return err.args[0], 404
 
+    def _serve_default_favicon(self):
+        headers = {
+            'Cache-Control': 'public, max-age={}'.format(
+                self.config.components_cache_max_age)
+        }
+        return flask.Response(pkgutil.get_data('dash', 'favicon.ico'),
+                              headers=headers,
+                              content_type='image/x-icon')
+
     def get_asset_url(self, path):
         asset = _get_asset_path(
             self.config.requests_pathname_prefix,
diff --git a/dash/version.py b/dash/version.py
--- a/dash/version.py
+++ b/dash/version.py
@@ -1 +1 @@
-__version__ = '0.27.0'
+__version__ = '0.28.0'


Test patch: