Using Subdomains in Django

Django’s url dispatcher doesn’t handle subdomains.  If you want/need to use subdomains you’ve got to use session middleware.  If you haven’t written any middleware yet don’t freak out, is easy.

There’s some easy documentation for extra information but for our needs all you need to do is make a Class with a function called process_request.  This function gets called on any request before it is processed by the url dispatcher.  It needs to return either nothing (None) or an HttpResponse object.  Here’s the code used to get the subdomain.

class SubdomainMiddleware:
    def process_request(self, request):
        """Parse out the subdomain from the request"""
        request.subdomain = None
        host = request.META.get('HTTP_HOST', '')
        host_s = host.replace('www.', '').split('.')
        if len(host_s) > 2:
            request.subdomain = ''.join(host_s[:-2])
Now your request object has a ‘subdomain’ attribute you can use in your views.  Alternatively you could return an HttpResponse of any sort including redirects directly from the process_request function.  Make sure to add this class to your middlware classes:
MIDDLEWARE_CLASSES = (
    'path.to.middlware.SubdomainMiddleware', )
Using subdomains on localhost can be a pain.  To do so sudo edit your /etc/hosts file and add the following lines replacing test.com with whatever you want to call your test url and the subdomains with your site’s subdomains.
127.0.0.1     test.com
127.0.0.1     subdomain1.test.com
127.0.0.1     subdomain2.test.com
This locally alters the DNS for test.com so you can use it as your localhost testing url.  Unfortunately the hosts file has no *.test.com functionality to handle redirecting all subdomains so this method only works if you have a known, finite set of subdomains.  If that’s not the case you’re going to have to use a development server or set up a proxy.

Update: I also forgot to mention that if you want your login sessions to work across all subdomains you can change the SESSION_COOKIE_DOMAIN variable as follows:

SESSION_COOKIE_DOMAIN = '.mysite.com'
Advertisements
26 comments
  1. Lynge said:

    Great, I was looking for something like this.
    One question though.
    If I dont set SESSION_COOKIE_DOMAIN, will any session only work on the subdomain that it was created/authenticated on?
    This would be great as it is just what I need.

    • @Lynge, yup you are exactly right. That’s how it’d work.

  2. Have you had any related issues with using subdomains?

    I am working on an art site and the subdomains are the usernames for the artists. The username/subdomain is an argument to the views which I have the middleware storing as setting and then passing this in to my urls via the optional arg dict.

    So my main two issues are
    1. I trying to link from one subdomain to another isn’t working (since the {% url %} tag doesn’t return the domain parts) and I can’t figure out how to pass in the optional dict args to the {% url %} to tell it which artist to use (regardless of the domain issue)

    2. This might be more of a setup thing, but sometimes when I load a path that exists on a subdomain, I randomly get either page. An apache restart seems to help. For example, sometimes http://example.com/somepage will show the content of http://sub.example.com/somepage Very weird.

    Have you had to address or solve either of these issues?

    • Sorry for the slow response, hopefully you’ve fixed this stuff by now.

      1. I’m pretty sure you can’t use subdomains in the url without some workarounds as the urlconf lookups work only off the request.get_full_path() (which doesn’t come with the domain much less the subdomain). One way to do it would be to create your own template tag, similar to {% url %} but that works with subdomains. Basically a somewhat more complicated wrapper around the ‘reverse’ function.

      2. Unless you tell it to, with middleware like this, Django isn’t going to be able to know the difference between http://example.com and http://sub.example.com/. In your view for ‘somepage’ you could right a check for the existence and value of the variable ‘request.subdomain’. If it doesn’t exist, then there was no subdomain so you should raise a 404 and yell at the user for trying to mess with your urls :).

  3. dodolboks said:

    hi.. i tray this Middleware and eror like this

    Traceback (most recent call last):

    File “/usr/lib/pymodules/python2.6/django/core/servers/basehttp.py”, line 279, in run
    self.result = application(self.environ, self.start_response)

    File “/usr/lib/pymodules/python2.6/django/core/servers/basehttp.py”, line 651, in __call__
    return self.application(environ, start_response)

    File “/usr/lib/pymodules/python2.6/django/core/handlers/wsgi.py”, line 230, in __call__
    self.load_middleware()

    File “/usr/lib/pymodules/python2.6/django/core/handlers/base.py”, line 40, in load_middleware
    mod = import_module(mw_module)

    File “/usr/lib/pymodules/python2.6/django/utils/importlib.py”, line 35, in import_module
    __import__(name)

    File “/home/dodolboks/riset/django/pyuer/../pyuer/middleware/pagesize.py”, line 54

    if len(host_s) > 2:

    ^

    SyntaxError: invalid syntax

    • Sorry I changed it. the $gt should be a ‘>’ greater than sign.

  4. dodolboks said:

    @godavemon

    i have views like this

    def pages(request, name):

    page = get_object_or_404(Entry, name=name)

    return render_to_response(‘pages.html’, {

    ‘page’ : page,

    }, context_instance=RequestContext(request))

    output http://example.com/test and i want redirect to http://test.example.com/

    and how using subdomains (SubdomainMiddleware) with above views

    many thanks

  5. dodolboks said:

    solved….

    great works guys..

    many thanks….

    • sphere said:

      dodolboks, how did you solved this ?

  6. rockham said:

    You could always just use PAC files for your browser to enable dynamic routing on your dev machine (instead of fiddling with /etc/hosts)

    At least that is what I do … very simple and easy 🙂

    // PAC proxy file for rails. Tested with Safari and Firefox

    function FindProxyForURL(url, host) {
    if (shExpMatch(host, “*.local”)) {
    return “PROXY localhost:3000”;
    }
    return “DIRECT”;
    }

  7. mikemike said:

    How do you recommend getting around somebody making their subdomain ‘ourwww’? When you delete ‘www.’, this will be removed. A more complex regex is required for a more robust version of this.

  8. @mikemike Excellent point. I’ll change it soon.

  9. mikemike said:

    @godavemon I used the wrong email before (hence my gravatar now). Here’s a new version which allows for settings (`offlimits` in case there’s more than just ‘www’ you wish to protect)

    offlimits = ['api', 'www', 'secure', 'blog']

    def get_safe_subdomain_or_none(host):
    subdomain = None
    L = host.split('.')
    if len(L) is 3 and not L[0] in offlimits:
    subdomain = L[0]
    return subdomain

  10. Olav said:

    Thanks a lot! I didn’t think it was going to be this easy when i heard from the IRC guys there was no builtin subdomain support in Django.

  11. Just found this little gem of a script. Incredibly useful Worked perfectly with the tweaks suggested in the comments.

  12. nav said:

    Dear Dave,

    Thank you for posting up this solution.

    I am trying out what you suggested but I am not able to get the URL to the middleware. The URL I am using is of the form subdomain.localhost:8000. I have also modified my /etc/hosts file as:

    127.0.0.1:8000 subdomain.localhost:8000

    must I disconnect myself from the internet for this to work? Or must I run the development server 127.0.0.1 without the port number. Kindly please shed some light.

    Cheers.
    nav

    • Thanks! I think you actually want this in your localhost

      127.0.0.1 subdomain.locahost

      Also I find its more useful to use more fun names other than localhost like

      127.0.0.1 subdomain.mysite-dev.com

      But yeah, the port number :8000 will just confuse the hosts file.

      • nav said:

        Thanks Dave for your reply.

        It all works now.

        Never would have guessed that the port number was confusing /etc/hosts.

        Cheers,
        nav

  13. Joe Tsai said:

    Kind of a late post, but if anyone cares… Rather than parse out the subdomain and store it in the request, I just wanted to be able to handle sub-domains via Django’s built in URL dispatcher.

    The following modification remains backwards compatible with the exist URL patterns. The modification changes the path_info value used by the dispatcher. Patterns used to identify paths originating from a subdomain simply need to search for the regex pattern ‘^/subname/’.


    '''
    Example:
    'domain.com' -> ''
    'domain.com/doc' -> 'doc'
    'domain.com/doc/' -> 'doc/'
    'sub.domain.com' -> '/sub/'
    'sub.domain.com/doc' -> '/sub/doc'
    'sub.domain.com/doc/' -> '/sub/doc/'
    'domain.com/foo' -> 'foo'
    'foo.domain.com' -> '/foo/'
    'sub1.sub2.domain.com' -> '/sub1_sub2/'
    '''

    class SubdomainMiddleware:
    def process_request(self, request):
    http_host = request.META.get('HTTP_HOST', '')
    sub_chunks = http_host.replace('www.', '').lower().split('.')[:-2]
    if len(sub_chunks) != 0:
    subdomain = "_".join(sub_chunks)
    request.path_info = "//" + subdomain + request.path_info

    Tested with Django 1.3.1 and Python 2.7

    • Ryan Anguiano said:

      Here’s how i solved the templatetag problem:

      from django.conf import settings
      from django.template import Library, TemplateSyntaxError
      from django.template.defaulttags import kwarg_re, URLNode
      
      register = Library()
      
      class SubdomainURLNode(URLNode):
          def render(self, context):
              url = super(SubdomainURLNode, self).render(context)
              if hasattr(settings, 'DOMAIN_SUFFIX'):
                  if url.startswith('//'):
                      parts = url[2:].split('/', 1)
                      url = '//%s.%s/%s' % (parts[0], settings.DOMAIN_SUFFIX, parts[1])
                  else:
                      url = '//www.%s%s' % (settings.DOMAIN_SUFFIX, url)
              return url
      
      
      @register.tag
      def url(parser, token):
          bits = token.split_contents()
          if len(bits) < 2:
              raise TemplateSyntaxError("'%s' takes at least one argument"
                                        " (path to a view)" % bits[0])
          viewname = parser.compile_filter(bits[1])
          args = []
          kwargs = {}
          asvar = None
          bits = bits[2:]
          if len(bits) >= 2 and bits[-2] == 'as':
              asvar = bits[-1]
              bits = bits[:-2]
      
          if len(bits):
              for bit in bits:
                  match = kwarg_re.match(bit)
                  if not match:
                      raise TemplateSyntaxError("Malformed arguments to url tag")
                  name, value = match.groups()
                  if name:
                      kwargs[name] = parser.compile_filter(value)
                  else:
                      args.append(parser.compile_filter(value))
      
          return SubdomainURLNode(viewname, args, kwargs, asvar, legacy_view_name=False)
      
  14. Ryan Anguiano said:

    And in my settings I created a DOMAIN_SUFFIX variable.

    On the production/staging server you would set it to example.com

    On my local machine, I added example.com to /etc/hosts and added the following to my settings:

    DOMAIN_SUFFIX = 'example.com:8000'
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s