Managing Redirects

We have a redirects app in bedrock that makes it easier to add and manage redirects. Due to the size, scope, and history of mozilla.org we have quite a lot of redirects. If you need to add or manage redirects read on.

Add a redirect

You should add redirects in the app that makes the most sense. For example, if the source url is /firefox/... then the bedrock.firefox app is the best place. Redirects are added to a redirects.py file within the app. If the app you want to add redirects to doesn’t have such a file, you can create one and it will automatically be discovered and used by bedrock as long as said app is in the INSTALLED_APPS setting (see bedrock/mozorg/redirects.py as an example).

Once you decide where it should go you can add your redirect. To do this you simply add a call to the bedrock.redirects.util.redirect helper function in a list named redirectpatterns in redirects.py. For example:

from bedrock.redirects.util import redirect


redirectpatterns = [
    redirect(r'^rubble/barny/$', '/flintstone/fred/'),
]

This will make sure that requests to /rubble/barny/ (or with the locale like /pt-BR/rubble/barny/) will get a 301 response sending users to /flintstone/fred/.

The redirect() function has several options. Its signature is as follows:

def redirect(pattern, to, permanent=True, locale_prefix=True, anchor=None, name=None,
             query=None, vary=None, cache_timeout=12, decorators=None):
    """
    Return a url matcher suited for urlpatterns.

    pattern: the regex against which to match the requested URL.
    to: either a url name that `reverse` will find, a url that will simply be returned,
        or a function that will be given the request and url captures, and return the
        destination.
    permanent: boolean whether to send a 301 or 302 response.
    locale_prefix: automatically prepend `pattern` with a regex for an optional locale
        in the url. This locale (or None) will show up in captured kwargs as 'locale'.
    anchor: if set it will be appended to the destination url after a '#'.
    name: if used in a `urls.py` the redirect URL will be available as the name
        for use in calls to `reverse()`. Does _NOT_ work if used in a `redirects.py` file.
    query: a dict of query params to add to the destination url.
    vary: if you used an HTTP header to decide where to send users you should include that
        header's name in the `vary` arg.
    cache_timeout: number of hours to cache this redirect. just sets the proper `cache-control`
        and `expires` headers.
    decorators: a callable (or list of callables) that will wrap the view used to redirect
        the user. equivalent to adding a decorator to any other view.

    Usage:
    urlpatterns = [
        redirect(r'projects/$', 'mozorg.product'),
        redirect(r'^projects/seamonkey$', 'mozorg.product', locale_prefix=False),
        redirect(r'apps/$', 'https://marketplace.firefox.com'),
        redirect(r'firefox/$', 'firefox.new', name='firefox'),
        redirect(r'the/dude$', 'abides', query={'aggression': 'not_stand'}),
    ]
    """

Differences

This all differs from urlpatterns in urls.py files in some important ways. The first is that these happen first. If something matches in a redirects.py file it will always win the race if another url in a urls.py file would also have matched. Another is that these are matched before any locale prefix stuff happens. So what you’re matching against in the redirects files is the original URL that the user requested. By default (unless you set locale_prefix=False) your patterns will match either the plain url (e.g. /firefox/os/) or one with a locale prefix (e.g. /fr/firefox/os/). If you wish to include this locale in the destination URL you can simply use python’s string format() function syntax. It is passed to the format method as the keyword argument locale (e.g. redirect('^stuff/$', '{locale}whatnot/')). If there was no locale in the url the {locale} substitution will be an empty string. Similarly if you wish to include a part of the original URL in the destination, just capture it with the regex using a named capture (e.g. r'^stuff/(?P<rest>.*)$' will let you do '/whatnot/{rest}').

Utilities

There are a couple of utility functions for use in the to argument of redirect that will return a function to allow you to match something in an HTTP header.

ua_redirector

bedrock.redirects.util.ua_redirector is a function to be used in the to argument that will use a regex to match against the User-Agent HTTP header to allow you to decide where to send the user. For example:

from bedrock.redirects.util import redirect, ua_redirector


redirectpatterns = [
    redirect(r'^rubble/barny/$',
             ua_redirector('firefox(os)?', '/firefox/', '/not-firefox/'),
             cache_timeout=0),
]

You simply pass it a regex to match, the destination url (substitutions from the original URL do work) if the regex matches, and another destination url if the regex does not match. The match is not case sensitive unless you add the optional case_sensitive=True argument.

Note

Be sure to include the cache_timeout=0 so that you won’t be bitten by any caching proxies sending all users one way or the other. Do not set the Vary: User-Agent header; this will not work in production.

header_redirector

This is basically the same as ua_redirector but works against any header. The arguments are the same as above except that thre is an additional first argument for the name of the header:

from bedrock.redirects.util import redirect, header_redirector


redirectpatterns = [
    redirect(r'^rubble/barny/$',
             header_redirector('cookie', 'been-here', '/firefox/', '/firefox/new/'),
             vary='cookie'),
]

Testing redirects

A suite of tests exists for redirects, which is intended as a reference of the redirects we expect to work on www.mozilla.org. This will become a base for implementing these redirects in the bedrock app and allow us to test them before release.

Installation

First follow the installation instructions for bedrock, which will guide you through installing pip and setting up a virtual environment for the tests. The additional requirements can then be installed by using the following commands:

$ source venv/bin/activate
$ pip install -r requirements/test.txt

Running the tests

Smoke tests are run against a local bedrock instance using -m smoke. These are also run automatically against GitHub pull requests.

$ py.test -r a -m smoke tests/redirects/

This will start a local instance of bedrock, run the smoke tests, and then stop the instance. If you wish to run the full set of tests, which requires a deployed instance of the site (e.g. www.mozilla.org) you can set the --base-url command line option:

$ py.test --base-url https://www.mozilla.org tests/redirects/

By default, tests will run one at a time. If you intend to run the suite against a remote instance of the site (e.g. production) it will run a lot quicker by running the tests in parallel. To do this, you can add -n auto to the command line. Replace auto with an integer if you want to set the maximum number of concurrent processes.