Skip to content

Integrate httparse for zero-copy HTTP request parsing#31

Open
jeeyo wants to merge 27 commits into
mainfrom
claude/integrate-httparse-4CWjj
Open

Integrate httparse for zero-copy HTTP request parsing#31
jeeyo wants to merge 27 commits into
mainfrom
claude/integrate-httparse-4CWjj

Conversation

@jeeyo

@jeeyo jeeyo commented Mar 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace hand-rolled HTTP/1.1 request parser with the httparse crate (no_std, zero-copy, zero-alloc)
  • Make HttpRequest<'a> fully zero-copy — path, query, headers, and body are all direct references into the connection's receive buffer, eliminating all data copying during parsing
  • Remove HttpRequest from Connection struct; parse on-demand using Rust's split borrows to simultaneously reference recv_buf (immutable) and response (mutable)
  • Delete ~80 lines of manual byte-scanning code (parse_request, find_header_end, memchr) and 6 buffer size constants (MAX_PATH_LEN, MAX_BODY_SIZE, etc.)

Test plan

  • west build -b native_sim compiles cleanly
  • west build -b rpi_pico2/rp2350a/m33 compiles for hardware target
  • GET /path?query=value with headers parses correctly
  • POST /path with body and Content-Length parses correctly
  • Partial/incremental requests wait for more data before processing
  • Malformed requests return 400 Bad Request

claude added 5 commits March 26, 2026 12:55
Use the battle-tested httparse crate (no_std, zero-copy, zero-alloc)
for HTTP/1.1 request parsing instead of manual byte scanning. This
improves RFC compliance and edge-case handling while reducing code by
~50 lines. The existing HttpRequest struct and all downstream consumers
remain unchanged.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
Replace fixed-size arrays in HttpRequest with references that borrow
directly from the receive buffer. This eliminates all data copying
during request parsing — method, path, query, headers, and body are
all zero-copy references into the connection's recv_buf.

Key changes:
- HttpRequest<'a> now holds &str for path/query, &[u8] for body,
  and httparse::Header<'a> array for headers
- Connection no longer stores HttpRequest; parsing happens on-demand
  in process_pending_requests() using split borrows
- handle_request() takes (&HttpRequest, &mut HttpResponse) separately
- Removed all request buffer size constants (MAX_PATH_LEN, etc.)

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
- Document httparse zero-copy parsing in architecture descriptions
- Add revised project roadmap to README.md (updated for Zephyr + Rust)
- Note HttpRequest<'a> zero-copy design in CLAUDE.md important notes

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
@jeeyo jeeyo changed the title Replace custom HTTP parser with httparse library Integrate httparse for zero-copy HTTP request parsing Mar 26, 2026
jeeyo and others added 4 commits March 26, 2026 22:14
Add 12 unit tests covering the test plan for httparse integration:
- GET with query string and headers
- POST with body and Content-Length
- All HTTP methods
- Partial/incremental request detection
- Malformed request rejection
- Multiple headers
- Empty buffer handling

Gate platform-dependent code (Zephyr, TCP, event loop) behind
#[cfg(not(test))] so cargo test compiles only the pure parsing
logic without requiring the Zephyr SDK.

Add a "Unit Tests" job to CI that runs cargo test independently
of the full Zephyr build.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
Add a bash script (scripts/integration_test.sh) that:
- Sets up a TAP interface for native_sim networking
- Launches the Zephyr native_sim binary
- Waits for the HTTP server to be ready
- Tests GET response status, headers (Server, Connection, Content-Type),
  and body (JSON-stringified handler output)
- Tests path/query propagation to the JS handler
- Tests POST with body
- Verifies handler console.log output in server logs

Add CI job that uploads the native_sim binary as an artifact from the
build job, then runs the integration test script with sudo.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
Comment thread .github/workflows/ci.yml Fixed
claude added 7 commits March 27, 2026 02:17
Zephyr native_sim connects to the TAP device named 'zeth' by default
without needing a CLI argument. The --eth-if flag is not supported.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
…ERNET

The integration test was hanging because native_sim was missing
CONFIG_POSIX_API=y (needed for POSIX socket names used by Rust FFI)
and CONFIG_NET_L2_ETHERNET=y. Also add --max-time to curl and a
3-minute timeout on the CI step as safety nets.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
On native_sim (compiled as host executable), extern "C" fn socket()
resolves to glibc's socket(), bypassing Zephyr's network stack and
TAP interface entirely. Using #[link_name = "zsock_socket"] etc.
ensures FFI calls always go through Zephyr's socket implementation.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
…lowdown

CONFIG_ETH_NATIVE_POSIX=y instantiates the TAP Ethernet driver (without
it, no packets flow through the TAP interface). CONFIG_NET_DRIVERS=y
enables the driver subsystem. CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=y
syncs simulated time to wall-clock time so TCP/ARP handshakes work
correctly with external tools like curl.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
…nking

zsock_* aren't exported symbols — they're accessed via C macros in
<zephyr/net/socket.h>. Add isere_* wrappers in app_shim.c that call
zsock_socket() etc. through the preprocessor, then use #[link_name]
in Rust FFI to bind to them. Same pattern as quickjs_shim.c.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
Use Zephyr's net_socklen_t type in socket shims (socklen_t is
unavailable on native_sim). Add CONFIG_ETH_NATIVE_POSIX,
CONFIG_NET_DRIVERS, and CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
On native_sim, socket FFI resolves to glibc — the server binds on the
host network. Integration tests curl localhost:8080 instead of needing
TAP interface setup. Zephyr's network stack is exercised on real
hardware (RP2350). Removed TAP/ETH configs and sudo from CI.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
Comment thread .github/workflows/ci.yml Fixed
claude and others added 11 commits March 27, 2026 07:23
Restrict GITHUB_TOKEN to read-only contents access per GitHub
security best practices (code scanning alert #10).

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
((PASS++)) when PASS=0 evaluates to ((0)) which returns exit code 1.
With set -e, the script exits on the first passing assertion. Use
PASS=$((PASS + 1)) instead, which is an assignment (always exit 0).

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
QuickJS bytecode is not portable across pointer sizes. The CI bytecode
job compiles with -m32 (for RP2350), but native_sim/native/64 needs
64-bit bytecode. Fix: only download pre-built 32-bit bytecode for ARM
builds; let CMakeLists compile bytecode at build time for native_sim
using CONFIG_64BIT to select the correct -m flag.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
Add NDEBUG to QuickJS compile definitions to disable the GC leak
detection assertion in JS_FreeRuntime. ES module objects can have
circular references that aren't collected before runtime teardown.
On RP2350 assert() is a no-op; on native_sim (glibc) it calls abort().
The runtime still frees all memory — the assertion is a debugging aid.

Also publish the RP2350 UF2 firmware as a CI build artifact.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
handler.bin is checked into git (32-bit placeholder). CMakeLists sees
it exists and skips building fresh bytecode. For native_sim (64-bit),
delete it before building so CMakeLists compiles 64-bit bytecode.

Also use SIGKILL in integration test cleanup — native_sim doesn't
handle SIGTERM, causing the script to hang on wait.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
Handler returns 200 with empty body and no console.log output.
Add printk tracing to http_handler (bytecode len, eval result,
poll iterations, completion status) and exception logging to
JS_ReadObject and JS_ResolveModule error paths.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
JS_READ_OBJ_ROM_DATA tells QuickJS to use the bytecode buffer directly
without copying. After the first request's runtime is freed, the shared
static buffer is corrupted, causing all subsequent requests to produce
empty modules (0 poll iterations, no handler output).

Removing the flag makes QuickJS copy the bytecode into its own heap on
each request. Costs ~1.3KB extra per request but fixes multi-request
handling. The memory is freed when the per-request runtime is dropped.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
ES module evaluation creates reference cycles (import/export bindings)
that survive QuickJS's normal GC sweep. Previously these leaked objects'
k_malloc memory was never reclaimed, exhausting the 128KB heap on the
second request. The NDEBUG workaround masked the assertion but didn't
fix the leak.

The patch now force-frees remaining GC objects in JS_FreeRuntime using
the same mechanism as gc_free_cycles(). Also fixes a use-after-free
where ContextOpaque was stack-allocated in setup_globals() but read
during poll() after that stack frame was gone.

https://claude.ai/code/session_013kzetaSHEMJeX8HufVQTqk
* fix: Correct async event loop polling and memory leak

- Replace `JS_FreeAtom` loop with `JS_FreePropertyEnum` to fix memory leak when getting property names
- Introduce `PollStatus` enum for `JsContext::poll` to properly differentiate between idle, executing jobs, and waiting for active timers
- Use `isere_k_msleep` (wrapped Zephyr `k_msleep`) to yield the thread when timers are active instead of spinning aggressively and exhausting iterations, which prevents the OS from firing the timers
- Print `JS_GetException` outputs during execution and evaluation functions to capture QuickJS exceptions and errors correctly

* fix: correct async event loop starvation and memory leak in JS handler

- Add missing `pull_request` triggers in `.github/workflows/ci.yml` so tests run on all PRs
- Replace `JS_FreeAtom` loop with `JS_FreePropertyEnum` to fix memory leak when getting property names
- Introduce `PollStatus` enum for `JsContext::poll` to properly differentiate between idle, executing jobs, and waiting for active timers
- Use `isere_k_msleep` (wrapped Zephyr `k_msleep`) to yield the thread when timers are active instead of spinning aggressively and exhausting iterations, which prevents the OS from firing the timers
- Print `JS_GetException` outputs during execution and evaluation functions to capture QuickJS exceptions and errors correctly

* fix

* fix

---------

Co-authored-by: jeeyo <1765881+jeeyo@users.noreply.github.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.

3 participants