Skip to content

fix(config): read the config file as UTF-8 rather than the platform codec - #396

Open
ppcvote wants to merge 1 commit into
finos:mainfrom
ppcvote:fix/config-loader-utf8
Open

fix(config): read the config file as UTF-8 rather than the platform codec#396
ppcvote wants to merge 1 commit into
finos:mainfrom
ppcvote:fix/config-loader-utf8

Conversation

@ppcvote

@ppcvote ppcvote commented Aug 20, 2026

Copy link
Copy Markdown

Closes #395.

The defect

load_from_file decoded the config with locale.getpreferredencoding():

# symphony/bdk/core/config/loader.py:27
config_content = config_path.read_text()

YAML and JSON are both specified as UTF-8, so on a non-UTF-8 host every non-ASCII value in a config comes back wrong.

The failure is silent where it matters. Measured on Windows with cp950, on a config carrying a non-ASCII proxy credential:

value
on disk password: "sésame-café" (b'...s\xc3\xa9same-caf\xc3\xa9...')
load_from_file password='s矇same-caf矇' — no exception
load_from_content, same bytes password='sésame-café'

Nothing errors. The bot starts, authenticates against the proxy with a password that is not the one in the file, and fails with a message that points nowhere near the config. The third row isolates it to the read rather than the parse.

A LC_ALL=C host raises instead, which is the loud version of the same bug; cp1252 on en-US Windows mangles it just as quietly as cp950 does.

The change

One argument on line 27. The repository already uses this pattern at service/user/user_service.py:798, so the two reads now agree.

Two other implicit-encoding reads are deliberately out of scope, rather than swept in because they matched a grep:

  • bdk_rsa_key_config.py:59 reads a PEM private key, which is base64 and therefore ASCII
  • on_disk_datafeed_id_repository.py:56 reads an id this library wrote itself

Neither can carry non-ASCII in practice. Happy to tighten them in a separate PR if you would like the module clean under -X warn_default_encoding, but they are a different question from this bug.

The test

Two deliberate choices, because the obvious version of this test proves nothing:

It runs the loader in a child interpreter with PYTHONUTF8=0 PYTHONCOERCECLOCALE=0 LC_ALL=C LANG=C. Without that it passes on a UTF-8 CI host whether or not the fix is present — which is why this has gone unnoticed.

load_from_content is used as the oracle. It receives already-decoded text and was always correct, so asserting the two entry points agree states the actual invariant rather than hard-coding an expected string.

The fixtures are written as raw bytes, and I verified the UTF-8 sequences survive into the git index (c3 a9 for é) — a fixture silently re-encoded on the way in would quietly disarm the test for everyone else.

Verified by reverting the one-line change: 4 failed / 1 passed before (both formats × both assertions), 5 passed after. The always-passing case is an ASCII config, kept as a control so a failure there means the harness broke rather than the encoding handling.

Checks

Full suite: 558 passed before, 563 after — exactly my five — with the same 2 pre-existing failures both times. Commit is DCO signed off; our FINOS CLA is on file.

…odec

BdkConfigLoader.load_from_file called read_text() with no encoding, so the
config was decoded with locale.getpreferredencoding(). YAML and JSON are both
specified as UTF-8, so any non-ASCII value was decoded with the wrong codec on
a non-UTF-8 host.

The failure is silent where it matters. Measured on Windows with cp950, a
config carrying a non-ASCII proxy credential:

  on disk : password: "sésame-café"   (b'...s\xc3\xa9same-caf\xc3\xa9...')
  loaded  : password='s矇same-caf矇'
  username: 'caf矇-user'

No exception. The bot starts and fails later against the proxy with an error
that points nowhere near the config. load_from_content on the same content
returns the right values, which isolates it to the read.

The repository already uses the correct pattern at
service/user/user_service.py:798, so this is one argument for consistency
with it.

Two other implicit-encoding reads are deliberately left out of scope rather
than bundled: bdk_rsa_key_config.py:59 reads a PEM key (base64, ASCII) and
on_disk_datafeed_id_repository.py:56 reads an id this library writes itself.

The test drives the loader in a child interpreter with PYTHONUTF8=0 LC_ALL=C
so it reproduces on a UTF-8 CI host, and uses load_from_content as the oracle
for what load_from_file should produce. Fixtures are written as raw bytes and
their UTF-8 sequences verified in the index, so no locale can influence them.
Verified by reverting the one-line change: 4 failed / 1 passed before, 5
passed after, the passing case being an ASCII control.

Full suite: 558 passed before, 563 after, same 2 pre-existing failures.

Closes finos#395

Signed-off-by: ppcvote <risky9763@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BdkConfigLoader.load_from_file decodes the config with the platform codec, silently corrupting non-ASCII values

1 participant