fix(parser): decode HTTP-fetched specs as UTF-8, not transport charset - #389
Open
sophie-jentic wants to merge 4 commits into
Open
sophie-jentic wants to merge 4 commits into
sophie-jentic wants to merge 4 commits into
Conversation
load_uri() read resp.text, which lets requests pick the codec from the HTTP Content-Type. For a text/* response with no charset parameter, requests follows the legacy RFC 2616 default and decodes as ISO-8859-1. That corrupts UTF-8 multibyte characters: an em-dash (E2 80 94) becomes bytes including 0x80, and PyYAML then rejects the stream with "unacceptable character #x0080" — the user-facing import failure seen when a server serves a valid UTF-8 spec as bare `text/yaml`. A YAML stream is UTF-8/16/32 by definition (YAML spec §5.2), so the transport charset must not decide the codec. Decode resp.content as utf-8-sig (which also strips a leading BOM), mirroring the encoding="utf-8" already used by the local-file branches. Keeps the -> str return contract, so all backends are unaffected. Adds tests/test_loader_encoding.py: the reported em-dash case, a non-ASCII and Content-Type matrix, BOM handling, all URL-loading backends end-to-end, local-file regression guards, error propagation, and a white-box guard that the loader never reads resp.text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes ruff F401 flagged by `poe lint`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes `ruff format --check` diff flagged by `poe lint`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI `poe typecheck` (pyright) flagged the new tests: - get_encoding_from_headers expects requests' CaseInsensitiveDict, not a plain dict; wrap the fake response headers accordingly. - monkeypatch.setattr(loader_module.requests, "get", ...) tripped reportPrivateImportUsage because the loader imports `requests` for internal use and does not re-export it. Patch via the string target "...core.loader.requests.get" instead, which is equivalent and avoids touching the private re-import. No behavior change; 61 tests still pass, ruff check/format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Problem
Importing a valid OpenAPI spec by URL fails with:
whenever the origin server serves the spec as
Content-Type: text/yamlwithout acharsetparameter (e.g.docs.livetennisapi.com), and the spec contains any non-ASCII character (an em-dash, curly quote, accent, emoji…).Pointing at a mirror that sends
charset=utf-8(e.g.raw.githubusercontent.com) "works", which is why this looked host-specific — but the byte content is identical; only the declared charset differs.Root cause
load_uri()readresp.text, which letsrequestschoose the codec from the HTTPContent-Type. For atext/*response with nocharset,requestsfollows the legacy RFC 2616 default and decodes as ISO-8859-1. That mis-decodes UTF-8 multibyte characters: an em-dash (—, UTF-8E2 80 94) becomes three Latin-1 code points, one of which is U+0080 — the exact character PyYAML's reader rejects.A YAML stream is UTF-8/16/32 by definition (YAML spec §5.2), so the transport charset must not decide the codec. Note the local-file branches in the same function already force
encoding="utf-8"; only the HTTP branch trusted the header.Fix
utf-8-sigalso strips a leading BOM).load_uri's-> strcontract, so all backends and the publicOpenAPIParser.load_uri()are unaffected.charset, not just one spec.Tests
New
tests/test_loader_encoding.py(61 tests):#x0080case (with a negative control proving the mis-decode is real).café, emoji, CJK, ellipsis) and aContent-Typematrix (baretext/yaml, declared utf-8,application/yaml, no header, misleadingcharset=iso-8859-1, …).resp.contentand neverresp.text.Validation: all 61 new tests pass; reverting the one-line fix fails 49 of them (the suite genuinely exercises the bug); full parser suite: 150 passed, no regressions.
🤖 Generated with Claude Code