Skip to content

[Detail Bug] Django templates: get_files does not deduplicate common chunks when skip_common_chunks=True #431

@detail-app

Description

@detail-app

Summary

  • Context: The django-webpack-loader template tags allow users to include assets from Webpack bundles in their Django templates, with an option to skip common chunks to avoid duplication.
  • Bug: The get_files template tag fails to track and skip already rendered common chunks when skip_common_chunks=True is used, because it does not update the _webpack_loader_used_urls attribute on the request object.
  • Actual vs. expected: get_files correctly filters against existing URLs in _webpack_loader_used_urls but fails to add its own returned URLs to this set, leading to subsequent tags (like render_bundle or another get_files) rendering the same assets again.
  • Impact: Duplicate assets (especially common vendor chunks) are loaded multiple times in the browser, increasing page size and potentially causing side effects in JavaScript execution.

Code with bug

@register.simple_tag(takes_context=True)
def get_files(
        context, bundle_name, extension=None, config='DEFAULT',
        skip_common_chunks=None):
    # ... (omitted setup)
    used_urls = getattr(request, '_webpack_loader_used_urls', None)
    if not used_urls:
        used_urls = set()
    if skip_common_chunks:
        result = [chunk for chunk in result if chunk['url'] not in used_urls]
    return result # <-- BUG 🔴 [The used_urls set is never updated or attached to the request if it was None]

Evidence

Reproduction Script

A reproduction script shows that calling get_files twice with skip_common_chunks=True returns the same files both times, and a subsequent render_bundle also includes them.

# First call
files1 = get_files(context, 'main', skip_common_chunks=True)
# Result: ['/static/bundles/common.js', '/static/bundles/main.js']

# Second call
files2 = get_files(context, 'main', skip_common_chunks=True)
# Result: ['/static/bundles/common.js', '/static/bundles/main.js'] 
# EXPECTED: []

Failing Test Case

A new test case in the project's test suite confirms the failure:

def test_get_files_deduplication(self):
    request = self.factory.get('/')
    template = Template(template_string=(
        '{% load render_bundle get_files from webpack_loader %}'
        '{% get_files "app1" skip_common_chunks=True as app1_files %}'
        '{% for f in app1_files %}<script src="{{ f.url }}"></script>{% endfor %}'
        '{% get_files "app2" skip_common_chunks=True as app2_files %}'
        '{% for f in app2_files %}<script src="{{ f.url }}"></script>{% endfor %}'
    ))
    output = template.render(context=Context({'request': request}))
    asset_vendor = '/static/django_webpack_loader_bundles/vendors.js'
    self.assertEqual(output.count(asset_vendor), 1) # Fails: returns 2

Why has this bug gone undetected?

get_files is less commonly used than render_bundle, and it's often used for prefetching or in scenarios where duplication might not be immediately obvious (e.g., hidden in a list of link tags). Developers using get_files might expect it to only return the files, and may not realize it needs to participate in the global deduplication state managed by render_bundle.

Recommended fix

Update get_files to match the behavior of render_bundle by ensuring used_urls is attached to the request and updated with the returned URLs.

    used_urls = getattr(request, '_webpack_loader_used_urls', None)
    if used_urls is None: # <-- FIX 🟢
        used_urls = set()
        setattr(request, '_webpack_loader_used_urls', used_urls) # <-- FIX 🟢
    if skip_common_chunks:
        result = [chunk for chunk in result if chunk['url'] not in used_urls]
    used_urls.update(chunk['url'] for chunk in result) # <-- FIX 🟢
    return result

History

This bug was introduced in commit 1792e80 (@karolyi, 2023-12-29, PR #386). This commit added the skip_common_chunks option to the get_files template tag but failed to include an update to the shared used_urls set, meaning files returned by this tag were never marked as rendered for subsequent tags.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions