fix(weather): enforce read deadline + size cap + percent-encode geocode (closes #203) - #205
Merged
ai-hpc merged 1 commit intoMay 27, 2026
Conversation
Contributor
|
reviewd, bounded weather HTTP reads and encoded geocode queries and merged at 15c2c95 |
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.
Summary
Three sibling concerns in
crates/genie-core/src/tools/weather.rs'sraw-TCP HTTP client, closed by one PR over one file:
http_get(pre-fix lines 182-228) had a 10 sTcpStream::connecttimeout but nothing onwrite_all/read_lineafterwards. A slow / hung Open-Meteo wedged the chat task forever.
Same crash mode as bug [bug] ha/client: only
TcpStream::connecthas a timeout — any hung HA reply blocks chat/voice/dashboard tool calls indefinitely #173 / PR fix(ha): enforce request-deadline + response-size cap on HA HTTP client (closes #173) #174 forha/client.misbehaving (or man-in-the-middle, since the connection is plain
HTTP per the file comment) response could grow RSS without limit.
non-hex lines with
push_str(no separator → silent JSON corruptionacross chunks) and misfired its heuristic on legitimate responses
whose first body char was a hex digit.
RFC 3986 char in the location (
&,?,#,=,%,+) leakedinto the geocode query string. "Q&A Cafe Tokyo" produced
name=Q&A+Cafe+Tokyo&count=1…— Open-Meteo parsed it asname=Qand silently returned no match.
Closes #203.
Changes
crates/genie-core/src/tools/weather.rs:WEATHER_CONNECT_TIMEOUT = 10s,WEATHER_REQUEST_TIMEOUT = 15s,WEATHER_MAX_RESPONSE_BYTES = 1 MiB.Doc-commented with the failure mode each one prevents.
http_get(host, path)becomes a thin wrapper around newpub(crate) async fn http_get_with_limits(host, port, path, connect_timeout, request_timeout, max_bytes)so the test module candrive an ephemeral mock listener at millisecond-scale.
tokio::time::timeout(connect_timeout, …)— clear"Open-Meteo connect to {addr} timed out"onElapsed.tokio::time::timeout(request_timeout, …)— clear"Open-Meteo GET {path} timed out after {N}s"onElapsed.read_http_get_body(reader, max_bytes): bails onTransfer-Encoding: chunkedwith an explicit"Open-Meteo response uses unsupported chunked encoding"(replacing the old silently-corrupting decoder); bounds the body
Stringwith a check-and-bail on every appended line.url_encode_query_param(s: &str) -> Stringpercent-encodes every non-RFC-3986-unreserved byte; multi-byte UTF-8 is
encoded byte-by-byte (e.g.
"München"→M%C3%BCnchen). Hand-rolled, no new dependency.
geocode()swapslocation.replace(' ', "+")forurl_encode_query_param(location).crates/genie-core/src/tools/weather.rs::tests— 5 new regressions:url_encode_query_param_percent_encodes_reserved_chars— unitcoverage on the helper: ASCII pass-through, every reserved char,
multi-byte UTF-8, empty input.
hung_server_after_connect_times_out_cleanly— mockTcpListeneraccepts and
sleep_forevers; assertsErr("…timed out…")insidethe test budget. Pre-fix this hangs the whole test process.
oversized_response_is_size_capped— listener streams 16 KiBchunks past a 64 KiB cap; asserts
Err("…exceeded…")before OOM.chunked_encoding_is_explicitly_rejected— listener returnsTransfer-Encoding: chunked; asserts the explicitErr("…chunked encoding…")instead of silent body corruption.geocode_request_line_contains_percent_encoded_location— listenercaptures the raw HTTP request line and echoes it back as JSON;
asserts the line contains
name=Q%26Aand does NOT containname=Q&A. Locks the URL-encoding fix at the wire level.No config schema change, no public API change. ASCII / space-only
locations against a healthy Open-Meteo see byte-identical behaviour.
Real Behavior Proof
scheduling and string formatting — no audio, voice, ALSA, CUDA, HA,
LLM-backend, or systemd surface — exercised end-to-end by 5 new
#[tokio::test]regressions that spin up real localTcpListenersand drive each failure mode.
What I ran
Environment: x86_64 Linux dev host (Ubuntu 22.04, Rust 1.95.0). No
Jetson available.
What I observed
TcpListenerthat acceptsthen
sleep_forevers,http_get_with_limits(…, 500ms, 500ms, …)returns
Err("Open-Meteo GET … timed out after 0s")in well undera second. Pre-fix the same call hangs for 600 s.
16 KiB chunks against a 64 KiB cap; the helper bails with
"Open-Meteo response exceeded 65536 bytes (got at least …)".Transfer-Encoding: chunkedwith a real chunk; helper bails with"Open-Meteo response uses unsupported chunked encoding". Pre-fixthis silently corrupted multi-chunk JSON.
request line via
readand echoes it back as JSON. Forgeocode("Q&A Cafe Tokyo")the captured line isGET /v1/search?name=Q%26A%20Cafe%20Tokyo&count=1&language=en&format=json HTTP/1.1.Pre-fix this would have been
name=Q&A+Cafe+Tokyo&…, whichOpen-Meteo parses as
name=Q.wmo_codes(existing test)still passes. Full
cargo test: 675 / 0 / 3 (up from main baseline~670 by exactly the 5 new tests).
--no-default-features: 570 / 0.Test plan
A reviewer can re-verify on any Rust 1.85+ host (no Jetson, no Open-
Meteo, no audio needed):
cargo test -p genie-core --lib tools::weather— 6 tests (1 existinggenie-corewith default configuration.curl -X POST http://127.0.0.1:3000/api/chat \ -H 'Content-Type: application/json' \ -d '{"message":"weather in Q&A Cafe Tokyo"}'.Pre-fix: returns "location not found". Post-fix: returns a
geocoded weather report for the closest matching location.
tcpdump -i any 'port 80'to seeGET /v1/search?name=Q%26A%20Cafe%20Tokyo&…on the wire.Notes for reviewers
crates/genie-core/src/tools/weather.rshas not been touched sincethe workspace scaffold commit
0a7363b. No open PR touches it.family (
http_get+ the three callers) and the test scaffolding(mock
TcpListener+ raw HTTP framing) is shared. Splitting themwould multiply review surface for the same total diff.
tier returns
Content-Length, never chunked. The pre-fix decodersilently corrupted multi-chunk JSON — refusing chunked explicitly is
safer than fixing a decoder for a path that should be unreachable.
A proper chunked decoder can land later if Open-Meteo ever changes.
typical 200 ms response. 1 MiB is ~50x the largest realistic forecast
response. Both are workspace-private constants; making them operator-
configurable can be a follow-up if needed.
read pattern in
ha/client. PR fix(llm): bound OpenAI-compat client timeouts to unwedge chat turn gate #182 (open) fixes the LLM client.This PR is the third leg — once it lands the "raw-TCP HTTP client
with insufficient read timeouts" class is closed across the
workspace.