FAILURE MAP
← Case archive

FA-211 / Protocols / Open access

A second percent-decoding pass changes path data into syntax · case 01

A literal plus becomes a space, or a double-encoded slash unexpectedly becomes a path separator.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Form-urlencoded rules or repeated decoding are applied to a path component that requires one decode pass.

VERIFIED REPAIR

Decode percent-encoded bytes exactly once with strict UTF-8 handling; preserve literal plus characters.

Unsuccessful approach: Decoding until the text stops changing interprets encoded percent signs as instructions for another pass.

Case contract

Decode a URL path component once using UTF-8. Preserve literal plus signs and malformed percent tokens; return None when encoded bytes are invalid UTF-8. Do not normalize paths or separators.

Why this case matters

Different decoding layers can disagree about a resource path. Keeping the decode count and plus-sign convention explicit prevents ambiguous interpretation.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
from urllib.parse import unquote, unquote_plus
N = 1
observations = []
def solve(text):
    try:
        return unquote_plus(text, errors='strict')
    except UnicodeDecodeError:
        return None
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
label = 'a' * N
check('literal plus retained in path', solve(label + '+b'), label + '+b')
check('encoded slash decoded once', solve(label + '%2Fb'), label + '/b')
check('double-encoded slash remains encoded', solve(label + '%252Fb'), label + '%2Fb')
check('encoded plus becomes literal plus', solve('%2B' * N), '+' * N)
check('UTF-8 encoded character', solve('%C3%A9' * N), 'é' * N)
check('malformed percent token preserved', solve(label + '%QZ%'), label + '%QZ%')
check('invalid UTF-8 rejected', solve('%FF'), None)
check('empty path component', solve(''), '')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
literal plus retained in patha ba+bFailed
encoded slash decoded oncea/ba/bPassed
double-encoded slash remains encodeda%2Fba%2FbPassed
encoded plus becomes literal plus++Passed
UTF-8 encoded characterééPassed
malformed percent token preserveda%QZ%a%QZ%Passed
invalid UTF-8 rejectedNoneNonePassed
empty path componentPassed

SHA-256 / b41b6d0e7846f34fc295afae1d547210b3d90a3352c1335006b90687471090bb

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
from urllib.parse import unquote, unquote_plus
N = 1
observations = []
def solve(text):
    try:
        while True:
            decoded = unquote(text, errors='strict')
            if decoded == text:
                return decoded
            text = decoded
    except UnicodeDecodeError:
        return None
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
label = 'a' * N
check('literal plus retained in path', solve(label + '+b'), label + '+b')
check('encoded slash decoded once', solve(label + '%2Fb'), label + '/b')
check('double-encoded slash remains encoded', solve(label + '%252Fb'), label + '%2Fb')
check('encoded plus becomes literal plus', solve('%2B' * N), '+' * N)
check('UTF-8 encoded character', solve('%C3%A9' * N), 'é' * N)
check('malformed percent token preserved', solve(label + '%QZ%'), label + '%QZ%')
check('invalid UTF-8 rejected', solve('%FF'), None)
check('empty path component', solve(''), '')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
literal plus retained in patha+ba+bPassed
encoded slash decoded oncea/ba/bPassed
double-encoded slash remains encodeda/ba%2FbFailed
encoded plus becomes literal plus++Passed
UTF-8 encoded characterééPassed
malformed percent token preserveda%QZ%a%QZ%Passed
invalid UTF-8 rejectedNoneNonePassed
empty path componentPassed

SHA-256 / e18f6174cecfbd2d7bcaf9d060e3468a509dcaa12bac9b62a9fe346ae468b78c

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
from urllib.parse import unquote, unquote_plus
N = 1
observations = []
def solve(text):
    try:
        return unquote(text, errors='strict')
    except UnicodeDecodeError:
        return None
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
label = 'a' * N
check('literal plus retained in path', solve(label + '+b'), label + '+b')
check('encoded slash decoded once', solve(label + '%2Fb'), label + '/b')
check('double-encoded slash remains encoded', solve(label + '%252Fb'), label + '%2Fb')
check('encoded plus becomes literal plus', solve('%2B' * N), '+' * N)
check('UTF-8 encoded character', solve('%C3%A9' * N), 'é' * N)
check('malformed percent token preserved', solve(label + '%QZ%'), label + '%QZ%')
check('invalid UTF-8 rejected', solve('%FF'), None)
check('empty path component', solve(''), '')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
literal plus retained in patha+ba+bPassed
encoded slash decoded oncea/ba/bPassed
double-encoded slash remains encodeda%2Fba%2FbPassed
encoded plus becomes literal plus++Passed
UTF-8 encoded characterééPassed
malformed percent token preserveda%QZ%a%QZ%Passed
invalid UTF-8 rejectedNoneNonePassed
empty path componentPassed

SHA-256 / 3150261a0255d06a41aedbe0409319a87636e7f76679f4390fc13158d2019935

Verification & scope

This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:36:51.476199+00:00.

Case digest / 67f45cf34b1344a1fd642d03fa1322c01378ee69bbb69b2b1eec3dcfeebca8bc