-
Notifications
You must be signed in to change notification settings - Fork 340
Description
Summary
- Context:
webpack_loader/loader.pyis a shim that imports all members fromwebpack_loader/loaders.pyto maintain backward compatibility after the module was renamed. The primary class it exports isWebpackLoader. - Bug: The
WebpackLoader.get_bundlemethod and themap_chunk_files_to_urlgenerator it returns both callself.get_assets()independently. This creates a race condition whenCACHEis disabled (as is common in development). - Actual vs. expected:
map_chunk_files_to_urlshould use the asset data already retrieved byget_bundleto ensure consistency. Currently, if the stats file is updated between the call toget_bundleand the first iteration of the returned generator, the chunk names will be inconsistent, leading to aKeyError. - Impact: Intermittent
KeyErrorcrashes occur during development when webpack finishes a build (updating the stats file) at the exact moment a page is being rendered.
Code with bug
# webpack_loader/loaders.py (re-exported by webpack_loader/loader.py)
def map_chunk_files_to_url(self, chunks):
assets = self.get_assets() # <-- BUG 🔴 [Re-loading assets here can result in a different version of the stats file than what was used to generate `chunks`]
files = assets["assets"]
add_integrity = self.config.get("INTEGRITY")
for chunk in chunks:
url = self.get_chunk_url(files[chunk]) # <-- BUG 🔴 [Raises KeyError if 'chunk' (from the old version) is missing from 'files' (from the new version)]Evidence
Reproduction Script
A reproduction script demonstrates that modifying the stats file between getting the bundle and iterating over it causes a KeyError.
import os, json, time
from django.conf import settings
from webpack_loader.loaders import WebpackLoader
# Configure minimal settings
if not settings.configured:
settings.configure(DEBUG=True, WEBPACK_LOADER={"DEFAULT": {"CACHE": False, "STATS_FILE": "stats.json", "BUNDLE_DIR_NAME": "bundles/", "TIMEOUT": None, "POLL_INTERVAL": 0.1, "IGNORE": [], "INTEGRITY": False}})
def write_stats(chunks, assets):
with open("stats.json", "w") as f:
json.dump({"status": "done", "chunks": {"main": chunks}, "assets": assets}, f)
# 1. Initial state with one chunk
write_stats(["chunk1.js"], {"chunk1.js": {"name": "chunk1.js"}})
loader = WebpackLoader("DEFAULT", settings.WEBPACK_LOADER["DEFAULT"])
loader.config["ignores"] = []
# 2. Get the bundle generator (calls get_assets() once)
bundle_gen = loader.get_bundle("main")
# 3. Modify the stats file before the generator starts (simulating webpack finishing a build)
write_stats(["chunk2.js"], {"chunk2.js": {"name": "chunk2.js"}})
# 4. Iterate (calls get_assets() again inside map_chunk_files_to_url)
print("Starting iteration...")
for chunk in bundle_gen: # Raises KeyError: 'chunk1.js'
print(chunk)Execution Output
Starting iteration...
Caught expected KeyError: 'chunk1.js'
Why has this bug gone undetected?
This bug primarily affects development environments where CACHE is set to False. In production, CACHE is typically True, which means get_assets() returns a cached object and avoids the race condition. Even in development, the race window is relatively small (the time between the two calls to get_assets()), making it an intermittent issue that developers might dismiss as a transient build artifact or a fluke of webpack being slow.
Recommended fix
Modify map_chunk_files_to_url to accept an optional assets argument, and pass the already-loaded assets from get_bundle.
# In WebpackLoader class:
def map_chunk_files_to_url(self, chunks, assets=None):
assets = assets or self.get_assets() # <-- FIX 🟢
files = assets["assets"]
# ... rest of the method
def get_bundle(self, bundle_name):
assets = self.get_assets()
# ... logic to find chunks ...
return self.map_chunk_files_to_url(filtered_chunks, assets=assets) # <-- FIX 🟢History
This bug was introduced in commit 7802555 (@gilmrjc, 2021-05-04, PR #232). This change updated the loader to support webpack-bundle-tracker v1, which changed the stats format to use chunk names instead of objects. The new implementation of filter_chunks added a second get_assets() call to look up these chunk details, creating a race condition when CACHE is disabled because the assets can change between the initial load in get_bundle and the second load in the generator.