Skip to content

Commit 14f4e82

Browse files
authored
feat: add a favicon route on the root (#17277)
* feat: add a favicon route on the root Many browsers may not respect the inline HTML directive on where to find a favicon file, so add a route to serve it directly and not respond with a 404 Not Found. Refs: #970 Signed-off-by: Mike Fiedler <[email protected]> * make translations Signed-off-by: Mike Fiedler <[email protected]> --------- Signed-off-by: Mike Fiedler <[email protected]>
1 parent cb2f973 commit 14f4e82

File tree

5 files changed

+56
-3
lines changed

5 files changed

+56
-3
lines changed

tests/unit/test_routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def add_redirect_rule(*args, **kwargs):
7777
pretend.call("force-status", r"/_force-status/{status:[45]\d\d}/"),
7878
pretend.call("index", "/", domain=warehouse),
7979
pretend.call("locale", "/locale/", domain=warehouse),
80+
pretend.call("favicon.ico", "/favicon.ico", domain=warehouse),
8081
pretend.call("robots.txt", "/robots.txt", domain=warehouse),
8182
pretend.call("opensearch.xml", "/opensearch.xml", domain=warehouse),
8283
pretend.call("index.sitemap.xml", "/sitemap.xml", domain=warehouse),

tests/unit/test_views.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import datetime
1414

15+
from pathlib import Path
16+
1517
import opensearchpy
1618
import pretend
1719
import pytest
@@ -25,6 +27,7 @@
2527
HTTPServiceUnavailable,
2628
HTTPTooManyRequests,
2729
)
30+
from pyramid.response import FileResponse
2831
from trove_classifiers import sorted_classifiers
2932
from webob.multidict import MultiDict
3033

@@ -363,6 +366,23 @@ def test_renders_503(self, pyramid_config, pyramid_request):
363366
_assert_has_cors_headers(resp.headers)
364367

365368

369+
def test_favicon(pyramid_request):
370+
pyramid_request.static_path = pretend.call_recorder(lambda path: f"/static/{path}")
371+
# Construct the path to the favicon.ico file relative to the codebase directory
372+
codebase_dir = Path(__file__).resolve().parent.parent.parent
373+
favicon_path = (
374+
codebase_dir / "warehouse" / "static" / "dist" / "images" / "favicon.ico"
375+
)
376+
# Create a dummy file to test the favicon
377+
favicon_path.parent.mkdir(parents=True, exist_ok=True)
378+
favicon_path.touch()
379+
380+
response = views.favicon(pyramid_request)
381+
382+
assert isinstance(response, FileResponse)
383+
assert pyramid_request.response.content_type == "image/x-icon"
384+
385+
366386
def test_robotstxt(pyramid_request):
367387
assert robotstxt(pyramid_request) == {}
368388
assert pyramid_request.response.content_type == "text/plain"

warehouse/locale/messages.pot

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
#: warehouse/views.py:149
1+
#: warehouse/views.py:157
22
msgid ""
33
"You must verify your **primary** email address before you can perform "
44
"this action."
55
msgstr ""
66

7-
#: warehouse/views.py:165
7+
#: warehouse/views.py:173
88
msgid ""
99
"Two-factor authentication must be enabled on your account to perform this"
1010
" action."
1111
msgstr ""
1212

13-
#: warehouse/views.py:301
13+
#: warehouse/views.py:332
1414
msgid "Locale updated"
1515
msgstr ""
1616

warehouse/routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def includeme(config):
2828
# Basic global routes
2929
config.add_route("index", "/", domain=warehouse)
3030
config.add_route("locale", "/locale/", domain=warehouse)
31+
config.add_route("favicon.ico", "/favicon.ico", domain=warehouse)
3132
config.add_route("robots.txt", "/robots.txt", domain=warehouse)
3233
config.add_route("opensearch.xml", "/opensearch.xml", domain=warehouse)
3334
config.add_route("index.sitemap.xml", "/sitemap.xml", domain=warehouse)

warehouse/views.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13+
from __future__ import annotations
1314

1415
import collections
1516
import re
17+
import typing
18+
19+
from pathlib import Path
1620

1721
import opensearchpy
1822

@@ -32,6 +36,7 @@
3236
from pyramid.i18n import make_localizer
3337
from pyramid.interfaces import ITranslationDirectories
3438
from pyramid.renderers import render_to_response
39+
from pyramid.response import FileResponse
3540
from pyramid.view import (
3641
exception_view_config,
3742
forbidden_view_config,
@@ -68,6 +73,9 @@
6873
from warehouse.utils.paginate import OpenSearchPage, paginate_url_factory
6974
from warehouse.utils.row_counter import RowCount
7075

76+
if typing.TYPE_CHECKING:
77+
from pyramid.request import Request
78+
7179
JSON_REGEX = r"^/pypi/([^\/]+)\/?([^\/]+)?/json\/?$"
7280
json_path = re.compile(JSON_REGEX)
7381

@@ -203,6 +211,29 @@ def service_unavailable(exc, request):
203211
return httpexception_view(HTTPServiceUnavailable(), request)
204212

205213

214+
@view_config(
215+
route_name="favicon.ico",
216+
decorator=[
217+
cache_control(365 * 24 * 60 * 60), # 1 year
218+
],
219+
)
220+
def favicon(request: Request) -> FileResponse:
221+
"""
222+
Return static favicon.ico file
223+
224+
The favicon path is not known, static files are compressed into a dist/ directory.
225+
"""
226+
favicon_filename = Path(
227+
request.static_path("warehouse:static/dist/images/favicon.ico")
228+
).name
229+
favicon_path = (
230+
Path(__file__).parent / "static" / "dist" / "images" / favicon_filename
231+
)
232+
233+
request.response.content_type = "image/x-icon"
234+
return FileResponse(favicon_path, request=request)
235+
236+
206237
@view_config(
207238
route_name="robots.txt",
208239
renderer="robots.txt",

0 commit comments

Comments
 (0)