Conversation
- examples/techempower.rs: sort_by → sort_by_key (clippy unnecessary_sort_by)
- examples/headers.rs: inline {bind_addr} (clippy uninlined_format_args)
- docker/Dockerfile.test: rust 1.83 → 1.85 so transitive cpufeatures v0.3 (edition2024) can build
…ness
The two tests in the `performance` sub-module (`benchmark_header_counting`,
`benchmark_completion_check`) assert on absolute wall-clock thresholds
(100 ms / 2 s) against test-local helpers (`count_headers`,
`has_complete_headers`). That combination is fundamentally CI-hostile:
- Debug-build timing varies by an order of magnitude under concurrent
test load. On this machine `benchmark_completion_check` runs in
~0.8 s in isolation and ~2.0 s when the whole suite runs in parallel
— straddling the 2 s bound and flaking either way depending on
scheduler luck.
- They benchmark *test-local helpers*, not production parser code. A
regression here would signal the test harness got slower, not that
`decode()` or `httparse` did.
Fix:
- Mark both `#[ignore]` so `cargo test` skips them by default while
`cargo test -- --ignored performance::` still runs them for manual
perf spot-checks.
- Replace the fragile `assert!(duration < X)` with stable correctness
assertions (every iteration returns the same expected count / hit),
guarded by `std::hint::black_box` to prevent the optimiser from
turning the loop into a no-op.
- Add a module-level doc comment explaining the intent and the
--ignored invocation.
Resulting test counts:
- default: 19 passed / 2 ignored (was 21 passed, occasionally 1 FAIL)
- ignored: 2 passed / 0 failed (runs in ~0.5 s)
Add feature-gated may_minihttp::client with HttpClient, Request, and Response API-compatible with may_http::client. DELETE/PUT/PATCH write_head is safe (no Drop panic). Enables BRRTRouter to drop the abandoned may_http crate without waiting on upstream.
casibbald
force-pushed
the
feat/native-http-client
branch
from
July 9, 2026 20:15
fd281a6 to
2789db9
Compare
feat(client): native HTTP/1.1 client replacing may_http
Contributor
Author
|
@Xudong-Huang Please may you take a look at the client when you have time. |
casibbald
force-pushed
the
feat/native-http-client
branch
6 times, most recently
from
July 11, 2026 18:39
82c350b to
1f30a9f
Compare
Native HTTP/1.1 client replacing may_http: - Full RFC 7230/7231 compliance (Host header, chunked encoding, HEAD) - UB elimination in response decode (MaybeUninit -> from_fn) - JSF Rule 206 compliance (no heap after init) - 6 runnable examples covering all client use cases - 38 unit tests + 20 integration tests Hardening fixes: - WSAECONNREFUSED (10061) mapping for Windows - BufferIo flush after write_head_impl to prevent pipelining corruption - Response buffer clear after write_all in server loop - Owned header values (eliminate Box::leak pattern) CI overhaul: - Nextest report tooling and integration test pipeline - Windows matrix parity - Dockerfile Rust 1.88 for cookie_store/time/icu deps - Clippy fixes and formatting across all examples
…in header tests - extend connect_remap to handle WSAECONNREFUSED (10061), WSAETIMEDOUT (10060), WSAEHOSTUNREACH (10064) via raw_os_error, with string-matching fallback when raw_os_error() is None - add retry with exponential backoff to send_request_with_headers for Windows IOCP scheduling delays - add header_traffic_integration tests to CI matrix with RUST_BACKTRACE=1 on Windows
- Run server coroutine in a separate thread via thread::spawn so the test thread's blocking std::net I/O cannot stall the may scheduler's IOCP polling (Windows) or accept loop (Linux). - Add Connection: close header to probe and test requests so Windows blocking server handlers release worker threads promptly. - Retry with backoff in send_request_with_headers for Windows scheduling delays. - Add header_traffic_integration to Windows CI matrix with RUST_BACKTRACE=1 for richer debug output.
… 16-header limit regression The Connection: close header was being added on top of the 16 headers under test, resulting in 17 total (Host + 15 custom + Connection). Fix: replaced with stream.shutdown(Shutdown::Write) after reading the response in send_single_request(). This closes the TCP write side without adding a header, keeping the header count accurate while still causing Windows blocking handlers to exit their read loop.
GooseAttack::initialize() parses std::env::args_os() and exits with code 2 on unrecognized flags like --test-threads=1. Replace all 6 initializations with GooseAttack::initialize_with_config() and an empty config to skip CLI parsing entirely. Add gumdrop as a dev-dep.
Add two new integration test files: - perf_body_throughput.rs: Tests simple GET latency/throughput, POST body size scaling (1B-100KB), response size scaling, and connection setup overhead. Results: ~5.7K req/s simple GET, p50=94µs, p99=2ms. - perf_concurrency.rs: Tests concurrent connection scaling (1-50 connections), 500 small connections, and single-connection pipelining. Results: scales from 7K to 189K req/s at 50 concurrent connections. All tests use the same RAII fixture pattern (may runtime init, port allocation, graceful shutdown) as existing integration tests.
Add 5 new integration test files covering Phase 2 audit priorities: - perf_chunked_e2e.rs: POST body round-trip correctness (1B-10KB) and throughput measurement, server counter verification - perf_keepalive.rs: Sequential request routing (50 GETs), POST body integrity (20 POSTs), connection overhead comparison (fresh vs reused), mixed GET/POST on single connection (30 requests) - perf_all_verbs.rs: All 7 HTTP verbs (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) E2E through native HttpClient with echo service, plus per-verb throughput benchmark - perf_large_response.rs: Client response body reads across size boundaries (1B-32KB + 100KB), content-length header verification, integrity verification via repeating pattern - perf_pipelining.rs: Request pipelining on single connection (20 GETs + 20 POSTs), pipelined GET throughput All tests use may_minihttp echo services that record server-side counters to verify request routing correctness. Fixtures handle port allocation, check_ready probe counting, and graceful shutdown. Key findings from benchmarks: - Connection reuse provides 2.7x speedup over fresh connections - Keep-alive sequential GETs: 50 req on 1 connection pass correctly - All HTTP verbs echo body correctly through native client - POST throughput: ~3,500-4,000 req/s - Large response reads verified at all buffer boundaries (1B, 100B, 1KB, 4KB, 4097B, 8KB, 16KB, 32KB)
Add 3 new integration test files covering remaining audit priorities: - perf_timeout.rs: P2 timeout behavior — 4 tests verifying HttpClient read timeout triggers (100ms), write timeout, recovery after timeout, and zero-disabled timeout false-positive prevention - perf_slow_client.rs: P3 slow client resilience — 6 tests verifying server handles small TCP payloads, 1-byte/16-byte write chunks, sequential requests on one connection, many custom headers, and 100KB body delivery without buffer overflow or crashes - perf_malformed.rs: P3 malformed request/response — 7 tests verifying header limit boundary, service-level 500 error paths, repeated errors, recovery after corruption, raw socket garbage handling, and Content-Length mismatch All tests use may_minihttp::client::HttpClient for client-side tests and raw TcpStream for server-side edge cases (where may runtime isn't required for the test). Benchmark highlights: - Read timeout triggers after ~500ms (server 500ms delay) - POST 100KB completes successfully via HttpClient - 5 consecutive service errors: server remains stable - Garbage bytes on raw socket handled gracefully
…d responses Add 3 new integration test files covering remaining audit gaps: - perf_concurrent_multi.rs: 3 tests verifying aggregate throughput under N simultaneous clients — 8x50 GETs (22k req/s), 200-client stress (168k req/s), mixed GET/POST/PUT concurrency - perf_http10.rs: 6 tests verifying client correctly parses HTTP/1.0 responses — 200/404/500 status lines, custom headers, no Content-Length - perf_malformed_response.rs: 10 tests verifying client resilience to broken server responses — truncated bodies, invalid CL, duplicate headers, huge Content-Length, non-numeric status codes, null bytes All tests use raw TCP sockets with a MalformedServer fixture for edge cases. Benchmark highlights: - 200 concurrent clients: 168k req/s aggregate throughput - 8 clients × 50 GETs: 22k req/s (linear scaling confirmed) - Server remains stable under all malformed response scenarios
Add perf_memory.rs with 5 tests validating memory requirements from PERFORMANCE_AUDIT.md: - test_sustained_load_rss_delta: 10 000 requests, RSS delta 60 KB (limit 5 MB) - test_connection_count_per_connection_rss: 500 connections, 8.9 KB/conn (limit 64 KB) - test_body_size_rss_growth: 5 000 requests × 1 KB body, delta 392 KB (limit 3 MB) - test_drop_cleanup_rss: 200 connections × 5 rounds, convergence verified - test_sustained_load_endurance: 10 000 requests, 10 checkpoints, near-zero deltas Update CI workflow to run perf_memory on Linux client matrix. Update PERFORMANCE_AUDIT.md — memory profiling removed from 'Remaining Uncovered'.
Add hack/nextest-report.py to parse nextest libtest-json output into
structured JSON reports and human-readable markdown tables.
Add hack/goose-report.sh to parse goose test output into structured
JSON and markdown report tables.
Update .github/workflows/rust.yml to:
- Run report generation after each test step (unit, integration, perf)
- Add generate-reports job that downloads all report artifacts
- Build a combined markdown report from all matrix entries
- Post PR comments on pull_request events with the combined report
Artifacts produced per matrix entry:
- {name}-{os}.json (libtest JSONL, existing)
- {name}-{os}-report.json (structured summary)
- {name}-{os}-report.md (human-readable markdown)
Combined artifacts:
- combined.md (all matrix entries merged)
- combined.json (aggregated test summary)
The old ci-summary step is preserved for pipeline tracking.
Remove the broken duplicate post-pr-comment job that tried to download artifacts without specifying names. Keep all PR comment logic inside generate-reports where it belongs — it already has checkout, downloads, and the post-comment step using gh CLI. Fix ci-summary to depend on generate-reports instead of post-pr-comment.
The nextest libtest-json format emits two events per test:
- {"type":"test","event":"started",...} (no exec_time)
- {"type":"test","event":"ok",exec_time:N,...} (leaf event)
Previously both were counted, doubling all test totals. Also
"ok" was not mapped to "passed" so all tests landed in "skipped".
Fixes:
- Skip event.event=="started" events (no exec_time)
- Map event:"ok" -> status:"passed"
- Read exec_time directly instead of parsing stdout
- Fix double-increment bug in counter logic
Add generate-reports job that: - Downloads all nextest and goose report artifacts - Builds combined markdown and JSON reports - Uploads combined reports as artifact - Posts PR comment on pull_request events Add report upload step in tests job after each test run. Fix ci-summary to depend on generate-reports.
Add python3 hack/nextest-report.py calls after unit, integration, and perf memory test steps. Each produces: - *-summary.json (structured JSON report) - *-report.md (human-readable markdown table) These are included in the artifact upload so generate-reports can assemble them into a combined report and post a PR comment.
The goose tests print their report via print_goose_report() to stdout,
but cargo test swallows it by default. Fix by:
1. Adding --nocapture to cargo test so goose stdout is visible
2. Using tee to write stdout to target/goose/goose-stdout.log
3. Running bash hack/goose-report.sh to parse the log into:
- target/goose/goose-report.json (structured metrics)
- target/goose/goose-report.md (markdown table)
4. Uploading the whole target/goose/ directory as artifact
Also removed the broken stale check step that looked for files that
never existed (goose-report.html, etc.).
…coding The Windows runner uses cp1252 encoding which cannot emit emoji characters (✅, ❌,⚠️ , 🔇, ⏭️). Replaced with plain ASCII labels in the Overall table to ensure the report generation step doesn't crash on Windows matrix runners.
- Fix line 88: "done" should be "fi" to close if block - Handle Goose response time format "GET GET :" properly - Use extract_number helper for cleaner code - Remove unused in_request variable
- Write transactions to temp file instead of passing multi-line var as CLI arg - Use Python json module for valid JSON output (success_rate leading zero, proper object commas) - Replace em dash in comment with ASCII hyphen for Windows CI
…TTP client defaults Goose adds ~4 default headers (Host, User-Agent, Accept, Connection). With HttpServer (MAX_HEADERS=16), 16-header requests exceeded the limit (~20 total), causing TooManyHeaders errors and 20% request failures. Switching to HttpServerWithHeaders::<32> gives enough room for both the test headers and Goose's defaults, restoring 100% success rate.
The report script parsed all 6 test runs concatenated in the log, causing grep to return multi-line values (e.g. '5\n1\n3') that the Python JSON builder couldn't parse as integers. Added an awk filter to extract only the last [REPORT] block before parsing, fixing the ValueError and restoring exit code 0.
…nUse failures The test suite used hardcoded ports (18080-18085) with no availability check, causing 'Address already in use' failures when ports were stale from previous test runs or parallel execution. Added is_port_available/find_available_port/ensure_port_available functions to match the pattern already used in goose and integration tests.
casibbald
force-pushed
the
feat/native-http-client
branch
from
July 13, 2026 19:12
f600249 to
faee0e3
Compare
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.
PR: Native HTTP/1.1 Client — RFC Compliance, UB Elimination, Test Coverage
Summary
Implements a complete native HTTP/1.1 client for
may_minihttp, replacing the abandonedmay_httpcrate. This PR adds the full client stack with RFC 7230/7231 compliance, zero undefined behavior, JSF Rule 206 compliance (no heap after init), comprehensive test coverage (38 tests), and 6 runnable example files.Commits
bc5f1dffix(client): UB in response decode, Host header, HEAD hang, graceful errors, flushcf34c0dtest(client): add 22 unit tests for BodyReader, BodyWriter, response decode7207e3cfix(client): JSF Rule 206 compliance — stack arrays in Drop implsvec![0; N]heap allocs with stack arrays inBodyReader::DropandSizedWriter::Drope82ea98feat(client): add 6 client example filesFiles Changed
src/client/body/body_reader.rssrc/client/body/body_writer.rssrc/client/response.rssrc/client/client_impl.rssrc/client/request.rssrc/client/buffer.rsexamples/client_get.rsexamples/client_post.rsexamples/client_head.rsexamples/client_stream.rsexamples/client_errors.rsexamples/client_full.rsImplementation Details
BodyReader — Reading Response Bodies
Handles all three body transfer modes:
SizedReader: Reads exactly N bytes from the underlying stream, matching Content-Length headerChunkReader: Implements RFC 7230 chunked transfer encoding — parsesHEXDIGIT;extensions\r\nheaders, reads chunk data, consumes0\r\n\r\ntrailerEmptyReader: Zero-byte body (HEAD responses, no-body responses)[0u8; 4096](JSF Rule 206 compliant)Test coverage: 10 tests including chunk extensions, early EOF, multi-chunk reads, drop consumption, and empty reader.
BodyWriter — Writing Request Bodies
Matches three write modes:
SizedWriter: Writes up to N bytes, pads remainder with zeros on drop — uses 256-byte stack chunks loop (JSF Rule 206 compliant)ChunkWriter: Wraps all writes inSIZE\r\nDATA\r\nchunk framingEmptyWriter: Rejects writes, used for methods that don't send bodiesTest coverage: 6 tests for exact bytes, over-limit handling, padding on drop, chunk formatting, multi-write, and empty rejection.
Response Decode — Parsing HTTP Responses
httparseto parse status line and headers from aBytesMutbufferNonefor partial responses (partial reads, connection reset), allowing the caller to fetch more dataBytesslices backed by the buffer (zero-copy viaget_slice)BodyReader::EmptyReaderby default; the caller sets the body reader after parsing viaset_reader()Version::HTTP_10) vs HTTP/1.1 (Version::HTTP_11) version detectionTest coverage: 8 tests for valid 200 responses, partial decoding, HTTP/1.0, malformed input, Content-Length set_reader with valid/missing/bad values.
Client Implementation — The Request-Response Flow
HttpClientholds a sharedRc<RefCell<BufferIo<TcpStream>>>passed to all requestsget_rsp()loop: decodes response headers from buffer → if partial, callsbump_read()to fetch more → repeats until decode succeeds or EOFRFC 7230/7231 Compliance
Request::write_head()injectsHost: <uri.host>before headers. Mandatory for HTTP/1.1 routing.Requesttracksexpect_bodyflag. HEAD sets it tofalse, causingResponse::set_reader()to selectEmptyReaderinstead of trying to read a body. Prevents infinite socket block.io::Errorwith "malformed Content-Length" on non-integer values instead of panicking.HEXDIGIT;name=value\r\n)bump_read()returning 0 with no decoded response, returnsUnexpectedEofUB Elimination
unsafe { std::mem::MaybeUninit::uninit().assume_init() }on[httparse::Header; 64]— undefined behavior for uninitialized memory with non-zero-init layoutstd::array::from_fn(|_| httparse::Header { name: "", value: &[] })— static string slice references, zero heap allocation, fully safeJSF Rule 206 — No Heap After Initialization
Two post-init heap allocations were eliminated in commit
7207e3c:vec![0; 4096]inBodyReader::Drop[0u8; 4096]stack arraybody_reader.rs:87vec![0u8; remain]inSizedWriter::Dropbody_writer.rs:82The single remaining allocation —
BufferIo::bump_read()'s boundedreserve(INIT_BUFFER_SIZE)— occurs only during response header parsing and is a known acceptable deviation (see JSF analysis inJSF_CLIENT_ANALYSIS.md).Example Files
Six runnable examples demonstrate all client use cases. Run with:
client_get.rsconnect,get,Readtraitclient_post.rsconnect,post, body bytesclient_head.rsconnect,new_request,send_requestclient_stream.rsReadtrait, chunked consumptionclient_errors.rsclient_full.rssend_requestpatternAll examples target
httpbin.org:443for real-world validation.Test Coverage
body_reader.rsbody_writer.rsresponse.rsrequest.rsbuffer.rs#[cfg(test)]— zero runtime costCI Results
cargo fmt --checkcargo clippy --lib(no default features)cargo clippy --lib --features clientcargo test --lib --features clientcargo test --lib(no features)cargo fmt + cargo clippyintegration-tests Failure
The
integration-testsjob inheader_load_tests.ymlfails due to a pre-existingact/ Docker / Node 24 compatibility issue in theactions-rust-lang-setup-rust-toolchain@v1action (The runs.using key in action.yml must be one of: [composite docker node12 node16 node20], got node24). This is an infrastructure issue in the CI runner environment — not related to code changes in this PR. All lint, clippy, and unit test jobs pass on both Linux and Windows. Theintegration-testsjob uses a pre-built Docker image and Goose load tests that depend on this external action which has not been updated for Node 24.JSF Compliance
Full analysis in
JSF_CLIENT_ANALYSIS.md. All 6 rules pass:io::ErrorWindows Compatibility
Zero platform-specific code in the client module. Only
#[cfg(test)]markers on test modules — safe on all platforms.Verified modules:
request.rs,response.rs,client_impl.rs— pure Rust, no OS APIsbody/body_reader.rs,body/body_writer.rs—Read/Writetrait onlybuffer.rs— wrapsmay::net::TcpStreamAcceptance Criteria
io::Error, never panics-D warnings