Template recursion: why does Django not allow `./` and `../`?

Hi. When trying to recursively include a Django template using relative paths that contain ./ or ../, Django raises a TemplateSyntaxError, which can be traced to line 268 of django/template/loader_tags.py. However, when using an “absolute” path instead (i.e. a relative path that does not use ./ or ../), Django does not throw any such error. Why is this a thing? Is there a reason that Django raises an error when using relative paths containing ./ or ../ but not with other ones?

For the sake of a minimal reproducible example, here are some steps to recreate the issue:

  1. Start a new Django project with django-admin startproject project.
  2. Change into the project directory and run django-admin startapp app.
  3. Add or modify the files according to the code snippets given below.
  4. Run the development server with python manage.py runserver.
  5. Visit http://localhost:8000 and confirm that the page displays a simple table of contents.
  6. In app/templates/app/ul.html, replace the path app/ul.html with ./ul.html.
  7. Visit http://localhost:8000 again and observe that the TemplateSyntaxError is produced.

app/views.py:

from django.shortcuts import render


def index(request):
    # Hierarchal data, from a database perhaps
    context = {
        "sections": [
            {
                "name": "Chapter 1: Mastering the art of foo",
                "sections": [
                    {
                        "name": "Section 1.1: What can foo do for you?",
                        "sections": [],
                    },
                    {
                        "name": "Section 1.2: A layman's guide to using a foobar",
                        "sections": [],
                    },
                ],
            }
        ],
    }
    return render(request, "app/ul.html", context)

app/templates/app/ul.html (after creating the app/templates/app/ directory):

<ul>
    {% for section in sections %}
        <li>
            <p>{{ section.name }}</p>
            {% if section.sections|length != 0 %}
                {% include "app/ul.html" with sections=section.sections %}
            {% endif %}
        </li>
    {% endfor %}
</ul>

app/urls.py (a new file):

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

project/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("", include("app.urls")),
]

project/settings.py (to install the app):

# ...

# Install the app we just created

INSTALLED_APPS = [
    "app.apps.AppConfig", 

    # ...
]

# ...

EDIT: I’ve modified the post title and text to clarify what I meant by “absolute” paths in the context of Django templates. The distinction is between paths that use ./ or ../ and paths that do not.

Welcome @gnpivo !

The easiest way to think about this is to realize that Django does a search for all templates. You’re actually not specifying an absolute path, you’re specify a path that is relative to the set of directories being searched in the TEMPLATES setting. As a result, there is no semantic meaning to the idea of a “current directory” in templates.

This line:

is a reference to a directory named app in any of the directories in the TEMPLATES search, not any one specific directory named app.

Hi @KenWhitesell, thank you for clarifying that detail. I’ve edited my earlier post to be more careful with my terminology. A path like app/ul.html is not actually an absolute path, but rather a path relative to the set of directories being searched by the template loader. Since the TEMPLATES setting in the project’s settings.py specifies APP_DIRS = True, the template loader will look in my app’s templates/ directory for the template as described in the Django documentation.

However, I still feel that the key mystery has not been solved. My understanding of the Django docs is that if a path begins with ./ or ../, it will be understood as a relative path from the directory of the template, as described in the Django docs for the extends tag (the include tag behaves as described in the docs for the extends tag).

It is not clear why the paths app/ul.html and ./ul.html should result in different behavior if they do indeed resolve to the exact same path (app/ul.html). In one case, the template compiles fine without problems. In the other case, an error is raised and the Django debug toolbar describes the error like this:

TemplateSyntaxError at /

The relative path ‘“./ul.html”’ was translated to template name ‘app/ul.html’, the same template in which the tag appears.

Why would this be an error? Was this a deliberate choice by the Django devs to prevent recursive template inclusion, but only with paths that use ./ or ../? Is there something I’m missing here?

Good question, one for which I do not see an answer.

None of the code in the relevent patch (Fixed #26402 -- Added relative path support in include/extends templa… · django/django@aec4f97 · GitHub), the original ticket (#26402 (Allow relative paths in {% extends %} and {% include %}) – Django), nor the referenced discussion (https://groups.google.com/g/django-developers/c/rDAJ0Ig6FoU) identify a reason for this specific test, other than it doesn’t make sense for a template to extend itself and the code added for the include tag uses the same test.

My gut reaction to this is that this could be considered a bug. I’d also make the guess that the right fix to this could be made without breaking anything that is currently working.

There’s a side note to this that is worth ensuring as being clear - it won’t only look in your app’s templates/ directory. It will search every installed app’s templates/ directory, including all of the Django apps and all third-party apps you may have listed in INSTALLED_APPS in addition to any directories specified in the DIRS list in the TEMPLATES setting. The first matching template name will be used.

Thanks again for the clarification. I took the initiative of opening a ticket and making a pull request. I would love to be able to brag to everyone I know that I contributed to the Django project (even if it was just to squash a tiny bug), and hopefully that dream will become a reality.

1 Like

Hey, I am actually a new contributor and looking forward to solve this issue, I had a solution for this and needed your views and guidance on this. So i have come to two conclusions that needs to be fixed here, one is when ./ and …/ are used within “include” it shoudn’t raise a error and other is when it extends itself it should raise an error. For the second the solution i think can help us is, in the loader_tags.py inside the ExtendsNode’s render function, we can add a block of code which checks if the value in the compiled_parent(parent template that the current template is extending.) is equal to the current template node, if both are same which refers to “extending the template within a same template” then we can raise a Templateerror with a message? This is the solution i thought of for extends part, please let me know if this approach is correct or not.

in this section i meant:

def render(self, context):
        compiled_parent = self.get_parent(context)

        if BLOCK_CONTEXT_KEY not in context.render_context:
            context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
        block_context = context.render_context[BLOCK_CONTEXT_KEY]

        # Add the block nodes from this node to the block context
        block_context.add_blocks(self.blocks)

        # If this block's parent doesn't have an extends node it is the root,
        # and its block nodes also need to be added to the block context.
        for node in compiled_parent.nodelist:
            # The ExtendsNode has to be the first non-text node.
            if not isinstance(node, TextNode):
                if not isinstance(node, ExtendsNode):
                    blocks = {
                        n.name: n
                        for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)
                    }
                    block_context.add_blocks(blocks)
                break

            # if compiled_parent in 

        # Call Template._render explicitly so the parser context stays
        # the same.
        with context.render_context.push_state(compiled_parent, isolated_context=False):
            return compiled_parent._render(context)

type or paste code here

Hi @YashRaj1506,

Sorry, but I had already pushed up a code fix when I made my original pull request. The approach I took was to edit that loader_tags.py file by modifying the construct_relative_path and do_extends functions so that the TemplateSyntaxError error is only raised when extending a template, not when including one. I also added some tests to ensure that relative recursive includes behave as expected without breaking other types of includes.

I am a new contributor as well, and I do appreciate your interest in contributing. Please feel free to take over if you still think there is work that needs to be done in order to address the issue.

Oh sorry, the issue was still open so i thought it wasn’t fixed. Really glad to see you worked on it. This was my first issue i planned to solved haha, will now look for other ones but even while going through it i learned a lot about how template works internally.