RSS Amplifier

EVILEG - Practical programming · Jun 3, 2025

Features of the resolve function behavior when setting the language in Django 5

0
Sign in to vote or save

Evgenii Legotckoi · EVILEG

Today I came across an interesting bug on EVILEG, which involves the behavior of the resolve function for resolving paths on a Django site when setting the language.

Introduction

Django has a feature for setting the user's language on the site. For example, if the site supports multiple languages, the user can choose English. In the standard Django package, there is a view for setting the language called set_language.

I used the code of this function to adapt it for my needs, as in the standard version I couldn't save the language settings for a registered user.

Here is the code I use:

def set_language(request, lang_code):
    next_url = request.GET.get('next', None)
    if (
            (next_url or request.accepts('text/html')) and
            not url_has_allowed_host_and_scheme(
                url=next_url,
                allowed_hosts={request.get_host()},
                require_https=request.is_secure(),
            )
    ):
        next_url = request.META.get('HTTP_REFERER')
        if not url_has_allowed_host_and_scheme(
                url=next_url,
                allowed_hosts={request.get_host()},
                require_https=request.is_secure(),
        ):
            next_url = '/'
    response = HttpResponseRedirect(next_url) if next_url else HttpResponse(status=204)
    if request.method == 'GET':  # Change method from POST to GET, we want use url parameters for this funcionality
        if lang_code and check_for_language(lang_code):
            if next_url:
                next_trans = translate_url(next_url, lang_code)
                if next_trans != next_url:
                    response = HttpResponseRedirect(next_trans)
            response.set_cookie(
                settings.LANGUAGE_COOKIE_NAME, lang_code,
                max_age=settings.LANGUAGE_COOKIE_AGE,
                path=settings.LANGUAGE_COOKIE_PATH,
                domain=settings.LANGUAGE_COOKIE_DOMAIN,
                secure=settings.LANGUAGE_COOKIE_SECURE,
                httponly=settings.LANGUAGE_COOKIE_HTTPONLY,
                samesite=settings.LANGUAGE_COOKIE_SAMESITE,
            )
            # Important part of code, set language to user, if user is authenticated
            if request.user.is_authenticated:
                request.user.language = lang_code
                request.user.save(update_fields=['language'])
    return response

This View can be connected as follows:

from django.urls import path
from myapp.views import set_language
urlpatterns = [
    path('lang/<lang_code>/', set_language, name='lang'),
]

By following the link, the user activates the selected language on the site.

Problem Description

Now let's move on to the description of the problem and the essence of the behavior of resolve, which is inside the translate_url function.

  1. When the user visits the site for the first time, they activate the language based on the browser settings. For example, by going to the url/en/posts/, the user activates the English language.
  2. Then the user activates the German language through set_language. In this case, everything works as expected, and the user ends up on the /de/posts/ page.
  3. Then the user changes the language code in the browser to Spanish and goes to the url /es/posts/. Everything works.
  4. Now the user tries to enable the German language again, BUT NOTHING HAPPENS. The user is still on the /es/posts/ page.
  5. This happens with every language the user tries to activate until they activate Spanish through set_language and try another language again. Then everything works as expected.

Essence of the Problem

The fact is that when the user has an active selected language, the resolve function, which is inside translate_url and checks the existence of a path on the site, will only work for those urls that have an active language or do not have a selected language at all.

In this code, with Spanish selected, we get an exception for all other languages in the url that are not active for the user:

def translate_url(url, lang_code):
    parsed = urlsplit(url)
    try:
        # URL may be encoded.
        match = resolve(unquote(parsed.path))
    except Resolver404:
        pass
    ...

Example

Spanish is active with the code es

Case 1

Translate /es/posts/ to English using translate_url.
Result: translate_url returns /en/posts/

Case 2

Translate /posts/ to English using translate_url.
Result: translate_url returns /en/posts/

Case 3

Translate /de/posts/ to English using translate_url.
Result: translate_url returns /de/posts/

Here resolve does not find the correct path on the site, so it does not perform the translation, even though the language is supported by the site.

Solution

In theory, this is a bug, and I need to check the Django tracker and report the details if I have time.

For now, in my case, I solved this problem by removing the language from the url, as everything works well without the language.

The result looks like this:

# -*- coding: utf-8 -*-
from urllib.parse import urlsplit, urlunsplit
from django.conf import settings
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import translate_url
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import check_for_language
def remove_language_code_from_path(path):
    for language in settings.LANGUAGES:
        if path.startswith(f'/{language[0]}/'):
            return path[len(f'/{language[0]}'):]
    return path
def remove_language_code_from_next_url(next_url):
    parsed = urlsplit(next_url)
    path = remove_language_code_from_path(parsed.path)
    return str(urlunsplit((parsed.scheme, parsed.netloc, path, parsed.query, parsed.fragment)))
def set_language(request, lang_code):
    next_url = request.GET.get('next', None)
    if (
            (next_url or request.accepts('text/html')) and
            not url_has_allowed_host_and_scheme(
                url=next_url,
                allowed_hosts={request.get_host()},
                require_https=request.is_secure(),
            )
    ):
        next_url = request.META.get('HTTP_REFERER')
        if not url_has_allowed_host_and_scheme(
                url=next_url,
                allowed_hosts={request.get_host()},
                require_https=request.is_secure(),
        ):
            next_url = '/'
        next_url = remove_language_code_from_next_url(next_url)
    response = HttpResponseRedirect(next_url) if next_url else HttpResponse(status=204)
    if request.method == 'GET':  # Change method from POST to GET, we want use url parameters for this funcionality
        if lang_code and check_for_language(lang_code):
            if next_url:
                next_trans = translate_url(next_url, lang_code)
                if next_trans != next_url:
                    response = HttpResponseRedirect(next_trans)
            response.set_cookie(
                settings.LANGUAGE_COOKIE_NAME, lang_code,
                max_age=settings.LANGUAGE_COOKIE_AGE,
                path=settings.LANGUAGE_COOKIE_PATH,
                domain=settings.LANGUAGE_COOKIE_DOMAIN,
                secure=settings.LANGUAGE_COOKIE_SECURE,
                httponly=settings.LANGUAGE_COOKIE_HTTPONLY,
                samesite=settings.LANGUAGE_COOKIE_SAMESITE,
            )
            # Important part of code, set language to user, if user is authenticated
            if request.user.is_authenticated:
                request.user.language = lang_code
                request.user.save(update_fields=['language'])
    return response

Read the original on evileg.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.