Repository navigation
Fixed
- A
langparameter the parser could not read no longer discards the MIME type
that carries it.langis cosmetic -- it says what language the body is
written in, not what the body is -- but failing to validate it returned
"invalid MIME type" for the whole header, and the caller answers that by
falling through to the spec'stext/geminidefault. So atext/plain
document came back withkind: gemtext, its declared charset dropped, and
its ordinary lines parsed as gemtext: any line beginning=>became an entry
indocument.links, a followablegemini://target indistinguishable from a
link the capsule actually published. That is the hazard the truncation path
already refuses -- half a link must not be presented as a whole one -- arrived
at from the other side.
The trigger did not even have to be malformed. The Gemini spec permits a
comma-separated list of tags, and the list was split on the comma but never
stripped, solang=en,frwas accepted andlang=en, fr-- the natural
spelling, and the one a server emits following ordinary parameter
conventions -- was not. An unusable value is now dropped at parse time and the
content type survives it.
The value that survives is rebuilt from the tags that were checked, so
lang=en, fris reported asen,fr. That is not cosmetic: validating a
stripped value while storing the raw one would admit every character
stripremoves -- CR, LF, U+2028, the separator controls -- into a field
that rides the one response path which does not otherwise sanitize, and so
reaches the model verbatim.
One behaviour change worth naming: a response whoselangwas malformed
used to reach the body-sniffing fallback, so atext/geminiheader over gzip
bytes was reported as binary. That was the bug's side effect, not a
safeguard -- the same body underlang=enwas decoded as gemtext, because
binary detection was never meant to hinge on the spelling of a language tag.
Sniffing now happens where it always should have: when the type itself is
unparseable. - A Gemini status line is checked for two ASCII digits.
str.isdigit()is
true of every Unicode decimal, so٢٠passed it andint()read it as 20 --
a status line the spec forbids was honoured as20 SUCCESSand its body
rendered. A different spelling of the same malformation,²⁰, passed the
check and then failed insideint(), escaping as a bareValueErrorthat
was reported to the caller asINVALID_REQUEST: the model was told its own
valid URL was malformed and sent to rewrite the one thing that was not the
problem. Both now raise a protocol error naming the response as the fault.
The Gopher parser already guarded its port field this way. - The Gopher cache is keyed on the parsed request rather than on the caller's
URL string, sorefreshinvalidates the resource instead of one spelling of
it. Host case was already folded, but nothing else was:gopher://host/0/a
andgopher://host:70/0/aare the same request and opened two entries, which
cost a second download and -- the sharp half -- meant arefreshspelled one
way left the other's copy in place. The next read was served the pre-refresh
body markedcached: true, which reads as merely old rather than superseded.
This is the same defect 0.10.1 fixed for windows of a resource, one level up;
the Gemini client already built its key from parsed components.
An internationalized host is folded to its A-label while parsing, which
parse_gemini_urlalready did and the Gopher parser did not.socket
IDNA-encodes a non-ASCII host before resolving it and Gopher sends no host
header, sogopher://bücher.example/…andgopher://xn--bcher-kva.example/…
reach the same server over byte-identical wire content -- one request, and so
one cache entry. Error messages andrequest_infonow report the A-label for
such a host.
Release Information:
- Version: 0.10.2
- Release Date: 2026-09-08
- Commit: 3ea88bb
- Workflow: 34175120877
Installation:
pip install gopher-mcp==0.10.2Verification:
All packages are signed and can be verified using Sigstore.