fix: make static asset URL canonicalization idempotent - #654
Merged
Syed-Ali-Abbas-568 merged 2 commits intoJun 29, 2026
Conversation
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
approved these changes
Jun 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Images whose filenames contain non-ASCII characters render with broken
srcURLs in unit HTML. An asset namedDía.jpg(Studio's Files section shows it correctly) ends up in the unit as...block@D%252525C3%252525ADa.jpgand 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_pathencodes the asset path withquote_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_info→replace_static_urls). Each cycle re-encodes the existing percent signs, so%C3%ADbecomes%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
%25layers. 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 usesunquoterather thanunquote_plusso 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-v1URLs thatreplace_static_urlsskips) needs a one-time repair. The newrepair_asset_url_encodingmanagement 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--commitis passed:Covered by new regression tests for the idempotent canonicalization and for the repair command's normalization helpers.
🤖 Generated with Claude Code