Skip to content

fix: make static asset URL canonicalization idempotent - #654

Merged
Syed-Ali-Abbas-568 merged 2 commits into
develop-teakfrom
ahmed-arb/fix-asset-url-double-encoding
Jun 29, 2026
Merged

fix: make static asset URL canonicalization idempotent#654
Syed-Ali-Abbas-568 merged 2 commits into
develop-teakfrom
ahmed-arb/fix-asset-url-double-encoding

Conversation

@ahmed-arb

Copy link
Copy Markdown
Collaborator

Images whose filenames contain non-ASCII characters render with broken src URLs in unit HTML. An asset named Día.jpg (Studio's Files section shows it correctly) ends up in the unit as ...block@D%252525C3%252525ADa.jpg and fails to load. The í is UTF-8 %C3%AD, but the % has been re-encoded several times into %252525.

The cause is that StaticContent.get_canonicalized_asset_path encodes the asset path with quote_plus, which does not treat % as a safe character, and Studio re-runs this over its own output on every load/save cycle (get_block_inforeplace_static_urls). Each cycle re-encodes the existing percent signs, so %C3%AD becomes %25C3%25AD, then %2525C3%2525AD, and so on, leaving the link broken once the real filename no longer resolves.

This fully percent-decodes the path before re-encoding it, making canonicalization idempotent: an already-canonicalized path now resolves to the same asset and produces the same single-encoded output, so repeated cycles no longer accumulate %25 layers. Decoding before the asset lookup also lets locked/digest/CDN handling find the real file rather than a key that still contains literal % characters. It uses unquote rather than unquote_plus so the + separators in opaque asset keys are preserved.

The platform fix prevents future corruption and heals at render time, but content already stored with the corrupted links (e.g. as absolute asset-v1 URLs that replace_static_urls skips) needs a one-time repair. The new repair_asset_url_encoding management command scans the data-bearing blocks of one or more courses, fully decodes over-encoded static/asset URLs, and rewrites course-owned absolute asset links back to the portable /static/<filename> form. It runs as a dry run unless --commit is passed:

./manage.py cms repair_asset_url_encoding course-v1:Org+Course+Run
./manage.py cms repair_asset_url_encoding --all --commit

Covered by new regression tests for the idempotent canonicalization and for the repair command's normalization helpers.

🤖 Generated with Claude Code

ahmed-arb and others added 2 commits June 27, 2026 14:42
Asset filenames with non-ASCII characters were progressively corrupted in
rendered unit HTML, e.g. an image named "Día.jpg" would render with a src of
...block@D%252525C3%252525ADa.jpg and fail to load.

StaticContent.get_canonicalized_asset_path encodes the asset path with
quote_plus, which does not treat '%' as a safe character. Studio re-runs this
function over its own output on every load/save cycle (get_block_info ->
replace_static_urls), so each cycle re-encodes the existing percent signs:
the 'í' in "Día" (UTF-8 %C3%AD) degrades into %25C3%25AD, then %2525C3%2525AD,
and so on, leaving broken <img> links.

Fully percent-decode the path before re-encoding so canonicalization is
idempotent: an already-canonicalized path now resolves to the same asset and
produces the same single-encoded output. Decoding before the asset lookup also
lets locked/digest/CDN handling find the real file instead of a key that still
contains literal '%' characters. unquote (not unquote_plus) is used so the '+'
separators in opaque asset keys are preserved.

Add a management command, repair_asset_url_encoding, to heal content already
stored corrupted: it scans the data-bearing blocks of one or more courses,
fully decodes over-encoded static/asset URLs, and rewrites course-owned
absolute asset links back to the portable /static/<filename> form. It is a dry
run unless --commit is passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repair command only handled multiply percent-encoded links, where the
non-ASCII bytes survive and can be decoded. A second corruption exists where the
non-ASCII character was dropped entirely (e.g. "climática" -> "climtica"), so the
link points at a filename that does not exist while the real asset still carries
the accent. Those references contain no information to decode, so the first pass
correctly left them alone -- and broken.

Add an asset-matching pass: for each course, build an index of the real asset
filenames from the contentstore and, when a decoded reference does not exist,
repoint it at the asset whose name with non-ASCII characters removed equals the
broken reference (the deterministic inverse of the corruption). Matches must be
unique; ambiguous or unmatched references are reported as WARN lines and left
untouched for manual review.

repair_text now returns (new_data, replacements, warnings) and normalize_asset_url
returns (new_url, status). Tests cover ascii_fold, AssetIndex.resolve (ok /
matched / ambiguous / unmatched) and the repoint-and-warn behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Syed-Ali-Abbas-568
Syed-Ali-Abbas-568 merged commit 1829f61 into develop-teak Jun 29, 2026
10 of 11 checks passed
@ahmed-arb
ahmed-arb deleted the ahmed-arb/fix-asset-url-double-encoding branch July 3, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants