| Milestone | Due | Focus |
|---|---|---|
| 1.35.5 | current branch | Docker builder mode, parallel builds, Go 1.26, Node 24 |
| 1.35.6 | 2026-06-25 | OTEL 0.24→0.32 (#65), OTLP/gRPC re-intro (config-selected protocol), Rust DX (rust1.x variant, libunit-rust) |
| 1.35.7 | 2026-07-31 | Short-cycle release |
| 1.35.8 | 2026-08-28 | Short-cycle release + Docker Hub Official Images (make library) |
Now (June):
| Task | Milestone |
|---|---|
| Finish PR #66 (builder mode, parallel builds, README) | 1.35.5 |
OTEL Phase 0: config audit, new fields, fake_otlp, test_otel.py |
1.35.6 |
OTEL Phase 2: rewrite nxt_otel_rs_runtime() for 0.32 API (after Phase 0) |
1.35.6 |
rust1.x Docker variant (WASM path, Dockerfile.rust1.x) |
1.35.6 |
Docker: make debug build optional (--debug flag in build-local.sh, off by default; saves ~30-40 MB per image) |
1.35.6 |
otel local image smoke-test: close doc/tag gaps (hardcoded 1.35.5, php-base snippet without --otel) |
1.35.6 |
July (after 1.35.6):
| Task | Milestone |
|---|---|
libunit-rust SDK — bindgen + FFI + axum adapter (prototype) |
1.35.7 |
Evaluate libunit-rust prototype → decide: WASM-first or native-first |
1.35.7 |
packages.freeunit.org — GoAccess / JSON stats for download counter |
1.35.7 |
fake_upstream prebuilt binary on packages.freeunit.org |
1.35.7 |
OTEL new config fields (service_name, headers, root_certificate, resource_attributes, max_queue_size, export_timeout) |
1.35.7 |
Rust toolchain bump: 1.94.1 → current stable (1.96); re-run otel clang-ast build + test_otel.py; decide pin-vs-floating for rust1.x image |
1.35.7 |
August 1 — start 1.35.8 work:
| Task | Milestone |
|---|---|
Prepare docker-library/official-images PR: uncomment make library in Makefile, update GitFetch, test metadata generation |
1.35.8 |
| Docker Hub Official Images: go through review process (docker-library/official-images PR template, CI validation) | 1.35.8 |
clang-ast plugin prebuilt binary on packages.freeunit.org |
1.35.8 |
libunit-rust — crates.io publish (if 1.35.7 prototype passes review) |
1.35.8 |
OTEL Phase 3: housekeeping ("NGINX Unit" → "FreeUnit", eprintln! → log_callback) |
1.35.8 |
Open questions (decide before August):
- OpenSSL 3.6 migration — verify clang-ast compatibility
- Proxy request buffering (#58) — define scope (per-action vs global)
Goal: attract Rust developers — the fastest-growing language community with no dedicated app server since Unit was archived. FreeUnit becomes the first app server with a native Rust workflow.
rust:1-slim-trixieconfirmed on Docker Hub — ships Rust 1.95.0,RUSTUP_HOME/CARGO_HOMEalready set, same layout as ourfreeunit-builder:trixie-rust1.94.1. Image pulled and verified locally.- All 21 trixie-based Dockerfiles already download Rust at build time (for wasmtime /
wasm-wasi-component) and then discard it. The rust variant just keeps it in the final
image — same pattern as
go1.xvariants. libunit-gois 676 lines of Go + 127 lines of C glue overnxt_unit.c(6800 lines). Path A forlibunit-rust(bindgen) is estimated at ~500–1000 lines of Rust — a reasonable scope for a single contributor.- Path B (pure Rust protocol reimplementation) rejected: too risky, too large, no benefit
until
libunit-rusthas proven adoption. - No existing Rust SDK for Unit found on GitHub (searched
unit-rs,libunit-rust,nginx-unit-rust). First-mover advantage available. - axum: 26k ⭐, actix-web: 24k ⭐ — Rust web ecosystem is mature and large.
- ngx-rust (https://github.com/nginx/ngx-rust) reviewed as reference. Longevity risk: F5-controlled, WIP, 60% commits from one engineer — use as pattern reference only.
Base image rust:1-slim-trixie already ships Rust 1.95.0 with RUSTUP_HOME /
CARGO_HOME configured (same layout as our builder images). Only additions needed:
rustup target add wasm32-wasip1inside the Dockerfile- Build FreeUnit with wasm + wasm-wasi-component modules (same as
Dockerfile.wasm) - Keep Rust toolchain in the final image — identical pattern to
go1.xvariants
Result: ghcr.io/freeunitorg/freeunit:latest-rust1.x — write, compile, and serve
Rust WASM apps from a single container. No external toolchain, no separate build step.
- Create
pkg/docker/Dockerfile.rust1.xbased onrust:1-slim-trixie - Add
rust1.xtorelease-docker.ymlCI matrix - Add
rust1.xtoALL_VARIANTSinbuild-local.sh - Add
rust1.xto variants table inpkg/docker/README.md - Test: compile a minimal axum→WASM app and serve via FreeUnit wasm runtime
Mirrors go/ library. Rust apps import libunit-rust, link libnxt_unit.a, and
speak the Unit app-worker protocol directly — no WASM compilation needed.
Architecture (same as Go):
nxt_unit.c (6800 lines) ← C protocol core, already battle-tested
↓ bindgen on nxt_unit.h
libunit-rust/src/ffi.rs ← generated FFI types (~auto)
libunit-rust/src/lib.rs ← safe wrappers (~500-1000 lines)
libunit-rust/src/axum.rs ← axum/hyper adapter (drop-in replace for std listener)
Reference files: go/unit.go, go/port.go, go/request.go, go/response.go,
go/nxt_cgo_lib.h.
- Run
bindgenonsrc/nxt_unit.h→libunit-rust/src/ffi.rs - Implement safe wrappers: port management, request/response, handler registry
- Add
axumadapter:ListenAndServe(handler)equivalent - Publish to crates.io as
libunit-rust - Add example app to
tools/or a separateexamples/rust/directory
| Today | After 1.35.6 | |
|---|---|---|
| Rust WASM apps | manual setup | docker pull freeunit:latest-rust1.x |
| Rust native apps | not supported | libunit-rust on crates.io |
| Developer story | "compile to wasm32-wasi manually" | one-liner |
Rust is the #1 most admired language (Stack Overflow 2024). No app server currently serves Rust developers. FreeUnit can own this space.
Crate upgrade 0.24 → 0.32 completed. Current crates: all 0.32 (see
src/otel/Cargo.toml). What remains: configurable fields, semconv attributes,
application-aware spans, test expansion.
Original implementation by Ava Hahn (@ava-affine).
The upstream nginx/nginx-otel is a separate C++ module, unrelated to our Rust crate.
- Bump all
opentelemetry*crates to 0.32 - Rewrite
nxt_otel_rs_init()— oldnew_pipeline()API gone in 0.27 - Dedicated-thread
BatchSpanProcessor+ blocking reqwest (no tokio runtime) -
ParentBased(TraceIdRatioBased(...))sampler — respects upstream sampling - Rename
"NGINX Unit"→"FreeUnit"in service name - Replace
eprintln!→nxt_otel_log_cbFFI callback - Remove dead
Protocol::HttpJsonarm (never reached) -
batch_sizevalidation bounds (1–65536) innxt_conf_validation.c -
sampling_ratiovalidation bounds (0–1) -
protocolenum —"http"+"grpc", both valid on any--otelbuild; transport chosen at runtime by config (upstream behaviour) - OTLP/gRPC export re-introduced — both transports compiled into every
--otelbuild (no separate flag);settings/telemetry/protocolselectshttp(blocking reqwest) orgrpc(tonic over a small owned tokio runtime); v1 plaintext h2c only -
fake_otlp— Rust mock OTLP collector (test/fake_otlp/) speaking both HTTP and gRPC in one build; HTTP path hardened (POST /v1/traces+application/x-protobuf+ non-empty body, else 400) -
test_otel.py— span export, traceparent, sampling, config validation; export cases parametrized over["http","grpc"]
Sources: nginx/nginx-otel (C++, gRPC-only, 10 tests), Caddy tracing module
(Go, gRPC, 9 unit tests + metrics). Angie = nginx-otel fork (not on GitHub, no
unique tests). FreeUnit has the only std-only mock collector + sampling-zero test.
| Attribute | nginx-otel | Caddy | FreeUnit (current) |
|---|---|---|---|
| HTTP method | ✅ http.method |
✅ auto (otelhttp) | ❌ "method" (not semconv) |
| URL/path | ✅ http.target |
✅ auto | ❌ "path" (not semconv) |
| Status code | ✅ http.status_code |
✅ auto | ❌ "status" (not semconv) |
| HTTP scheme | ✅ http.scheme |
✅ auto | — |
| HTTP flavor | ✅ http.flavor |
✅ auto | — |
| User agent | ✅ http.user_agent |
✅ auto | — |
| Request body size | ✅ http.request_content_length |
— | ❌ "body size" |
| Response body size | ✅ http.response_content_length |
— | — |
| Server name | ✅ net.host.name |
— | — |
| Client address | ✅ net.sock.peer.addr |
— | — |
| Error on 5xx | ✅ span.setError() |
— | — |
| Application name | — | — | — |
| Application type | — | — | — |
FreeUnit differentiator — application-aware spans. Unlike nginx/Caddy (reverse proxies), FreeUnit is an app server: it knows which application handled the request. When multiple PHP versions or Go+Python apps run side-by-side, OTel spans must identify the target:
| Attribute | Source | Example |
|---|---|---|
unit.application.name |
r->app_name from router |
"wordpress", "api-v2" |
unit.application.type |
language module (php/python/go/...) | "php", "python", "go" |
unit.application.processes |
app conf | "5" |
These are FreeUnit-specific — no competitor has them. Every add_event call
in nxt_otel.c should include these alongside the standard semconv attributes.
| Test case | nginx-otel | Caddy | FreeUnit |
|---|---|---|---|
| Span exported | ✅ | ✅ | ✅ |
| traceparent inject | ✅ (4 modes) | ✅ | ✅ |
| traceparent inherit | ✅ | ✅ | ✅ |
| Sampling zero | — | — | ✅ unique |
| $otel_trace_id variable | ✅ | ✅ | — |
| Custom span attributes | ✅ | ✅ | — |
| Custom resource attributes | ✅ | ✅ | — |
| Exporter headers (auth) | ✅ | ✅ | — |
| TLS export | ✅ | ✅ | — |
| Trace off | ✅ | — | — |
| Batching | ✅ | — | — |
| HTTP/2.0, 3.0 | ✅ | — | — |
| Feature | nginx-otel | Caddy | FreeUnit (current) |
|---|---|---|---|
| Transport | gRPC only | gRPC (autoexport) | HTTP only |
| Trace context modes | ignore/extract/inject/propagate | auto (autoprop) | inject only |
| $otel_trace_id etc. | ✅ 4 variables | ✅ 2 placeholders | — |
| Custom span name | ✅ otel_span_name |
✅ span directive |
— |
| Custom span attrs | ✅ otel_span_attr |
✅ span_attributes |
— |
| Resource attributes | ✅ otel_resource_attr |
✅ (env + semconv) | "FreeUnit" hardcoded |
| Exporter headers | ✅ header directive |
✅ OTEL_EXPORTER_OTLP_HEADERS |
— |
| TLS to collector | ✅ trusted_certificate |
✅ (env) | — |
| OTEL_* env vars | — | ✅ | — |
| Metrics | ❌ | ✅ (separate subsystem) | ❌ |
1.35.6 (current, close before release):
- Fix span attributes to use OTel semconv:
"method"→http.request.method,"path"→url.path,"status"→http.response.status_code - Add application-aware attributes:
unit.application.name,unit.application.typefromr->app_name/ language module innxt_otel.c - Add missing standard attributes:
http.scheme,http.flavor,http.user_agent,server.address,client.address - Pass
tracestatethrough to Rust OTEL SDK (parsed in C atnxt_otel.c:376, stored inr->otel->trace_state, but never forwarded to Rust) - Add
setError()equivalent on HTTP 5xx responses - Harden
fake_otlprequest validation (PLAN.md WS-B): validatePOST /v1/traces,Content-Type: application/x-protobuf, reject empty bodies
1.35.7 (July):
- New config fields:
service_name,headers,root_certificate,resource_attributes,max_queue_size,export_timeout -
$otel_trace_id,$otel_span_id,$otel_parent_idvariables (access log + response headers — nginx-otel has 4 variables, Caddy has 2 placeholders) - Custom span attributes directive (per-route, like nginx-otel
otel_span_attr) - Evaluate
OTEL_*env vars as fallback/override forsettings.telemetry(Caddy pattern — interop with existing OTel deployments) - Expand
test_otel.py: custom span attrs, resource attrs, auth headers, trace context modes
1.35.8 (August):
- gRPC transport re-introduction — landed early in 1.35.6, config-selected
protocolin every--otelbuild (see "Done in 1.35.6") - TLS to collector (
root_certificate+ reqwest rustls/native-tls; gRPC v1 is plaintext h2c only) - Metrics exploration (Caddy has full HTTP metrics via OTLP; nginx-otel has none)
nxt_otel_rs_get_or_create_trace() returns Box::into_raw(...) into
r->otel->trace. The span is reclaimed only via NXT_OTEL_COLLECT_STATE
(nxt_otel_span_collect). nxt_otel_request_error_path() correctly sends trace
through COLLECT for error cases, but if the request is torn down between INIT and
COLLECT without hitting the error path, r->otel->trace leaks.
- Add a teardown path that calls
nxt_otel_rs_send_trace()whenr->otel->traceis non-null and COLLECT was not reached.
Shipped in 1.35.6. Both transports compiled into every --otel build;
settings/telemetry/protocol picks http/grpc at runtime — same UX as
upstream Unit (which the public site already documents). Key decisions:
- tokio already linked transitively (reqwest → hyper → tokio 1.52.3), so the
delta is tonic +
grpc-tonic+ a small runtime this crate owns - Precedent:
wasm-wasi-componentruns managed tokio runtime on dedicated thread - Considered an opt-in
--otel-grpcCargo feature but rejected it: it changed the historical config-only UX and split the build matrix. Always-on keeps one binary, one config contract. - v1 is plaintext h2c only — no TLS to collector (tracked under 1.35.8 TLS item)
- See
test/fake_otlp/PLAN.mdfor the gRPC test infrastructure (all boxes ticked)
- nginx/nginx-otel (C++, gRPC-only, port 4317, TLS since 0.1.2): https://github.com/nginx/nginx-otel — docs: https://nginx.org/en/docs/ngx_otel_module.html
- NGINX OTel admin guide: https://docs.nginx.com/nginx/admin-guide/dynamic-modules/opentelemetry/
- Angie OTel module (gRPC-only, nginx-otel fork): https://en.angie.software/angie/docs/installation/external-modules/otel/
- Caddy
tracing(opentelemetry-go, gRPC for traces, OTEL_* env config): https://caddyserver.com/docs/caddyfile/directives/tracing - Caddy #5743 — request to add OTLP/HTTP for traces: caddyserver/caddy#5743
- FrankenPHP #1715 — Caddy(gRPC) vs PHP(HTTP) env var conflict: php/frankenphp#1715
- OTLP spec (4317 gRPC / 4318 HTTP; default SHOULD be http/protobuf): https://opentelemetry.io/docs/specs/otlp/
- OTLP exporter config (per-SDK defaults; Go=grpc, Node=http/protobuf): https://opentelemetry.io/docs/specs/otel/protocol/exporter/
- opentelemetry-php (HTTP/protobuf, port 4318): https://github.com/open-telemetry/opentelemetry-php
In-repo audit (2026-05-31):
- tokio in otel staticlib via reqwest-blocking → hyper/tower (
src/otel/Cargo.lock) - managed tokio runtime precedent:
src/wasm-wasi-component/src/lib.rs - gRPC origin: upstream commit
8b697101"otel: add opentelemetry rust crate code" (http+grpc) - Rust toolchain: pinned 1.94.1, Rust 1.96 just released (6-week cadence; memory-safe,
no GC, runtime limited to std-lib init — fits embedded staticlib). The floating
rust:1-slim-trixiebase forrust1.xvariant now tracks 1.95→1.96. Bump pinned toolchain → current stable, re-run otel clang-ast +test_otel.pyto confirm 0.32 crates compile clean on newer Rust. Decide pin-vs-floating policy forrust1.ximage (floating drifts; pin for reproducible CI).
Before the OpenSSL 3.6 migration can be considered fully validated:
- Verify that the
openssl-3.xbranch (if it exists upstream or as a fork reference) still applies cleanly on top ofmasterwith the newOBJ_sn2nid/OpenSSL_version_numreplacements. - Run the full CI matrix (
build-test.yml) and confirm the new "Build OpenSSL 3.6" step succeeds on bothamd64andarm64runners. -
clang-astworkflow passes ondebian:testing+ system OpenSSL 1.1 via./test/run-local-full.sh(verified onpre-1.35.5branch). - Confirm
clang-aststill passes when linked against OpenSSL 3.6 (previously broken byEVP_PKEY_asn1_find_str/SSLeaydeprecations — fixes need re-verification on the 3.6 build). - Smoke-test TLS in a Docker image built from
Dockerfile.minimal(nowdebian:trixie-slim) — load a certificate via the REST API and make an HTTPS request. - Investigate
eclipse-temurin:11-jdk-noble(Ubuntu 24.04, OpenSSL 3.3) as the one remaining image that does NOT reach OpenSSL 3.6; decide whether to build OpenSSL 3.6 from source in that Dockerfile or accept the gap until eclipse-temurin gains a Debian trixie variant.
PHP 8.5 removed the disable_classes INI directive (deprecated since 8.4).
Unit passes it via php_admin_value in nxt_php_sapi.c — PHP 8.5 ignores it silently,
causing test_php_application_disable_classes and test_php_application_disable_classes_user to fail.
Tests: test/test_php_application.py — skipped for PHP >= 8.5 with explicit reason.
Fix needed:
- Remove or conditionalize
disable_classeshandling insrc/php/nxt_php_sapi.c - Consider returning an error from the config API if
disable_classesis set with PHP 8.5+ - Or document the removal and drop the feature
packages.freeunit.org serves tarballs (njs, wasmtime, wasi-sysroot, libunit-wasm)
but there is no download counter — no visibility into which packages are downloaded
and how often.
Server runs Angie (nginx-compatible fork, COMBINED log format).
Options (ascending complexity):
- GoAccess — install on server, parse Angie access log, publish HTML report to
packages.freeunit.org/stats/ - JSON counter via cron — hourly
awkover access.log →stats.json, enables badge endpoints or API consumers - Angie NJS counter — shared-memory counter incremented per
.tar.gzrequest, exposed as/metricsendpoint (no log parsing needed, real-time)
Quick start (GoAccess):
sudo apt-get install -y goaccess
goaccess /var/log/angie/packages.freeunit.org.access.log \
--log-format=COMBINED -o /var/www/packages.freeunit.org/stats/index.htmltest/fake_otlp/ is built and used by test/test_otel.py in 1.35.6, but its
handle() (src/main.rs) currently counts any non-empty request as a
received span. It does not assert the request is a real OTLP export. Kept as-is
for now (the export tests pass); harden later so the mock can't be satisfied by
garbage:
- Validate request line is
POST /v1/traces - Validate
Content-Type: application/x-protobuf - Reject empty/zero-length protobuf body (currently only the readiness probe with a fully empty buffer is filtered)
- Optionally decode the protobuf far enough to confirm at least one span
The Phase 0 design (above) already specifies this behavior; the shipped binary implements only the empty-probe guard. This item tracks closing that gap.
test/fake_upstream/ — Rust HTTP mock used by test_proxy_chunked.py.
Currently built from source in Docker (cargo build --release), adding ~0.5s per run.
Improvement:
- Build
fake_upstreambinary and publish topackages.freeunit.org - Update
run-local.shto download prebuilt binary instead ofcargo build - Add SHA-512 checksum validation (like
pkg/contrib/Makefiledoes for njs/wasmtime) - Fallback to cargo build if download fails
Benefits:
- Faster test image builds
- Reproducible binaries across platforms (AMD64 + ARM64)
- No Rust toolchain required in Docker image
test/run-local-full.sh builds a Docker image for clang-ast analysis.
Fixed: use clang llvm-dev libclang-dev (not clang-21 llvm-21-dev libclang-21-dev).
Current state: Works on debian:testing (clang 21 + llvm 21).
Future improvements:
- Prebuild
freeunit-test-full:localimage and publish to GHCR - Or add packages.freeunit.org binary for clang-ast plugin
- Cache Docker layers for apt install + clang-ast build
otel coverage in local image smoke-tests — already exists; close the doc/tag gaps (milestone 1.35.6)
Scheduled for the 1.35.6 release — close the doc/tag gaps below as part of the same milestone as the OTel 0.24→0.32 upgrade (#65).
otel runtime-build coverage is already in place in two paths; the gaps are documentation and a hardcoded version, not missing infrastructure:
test/run-local.shtest image (FROM python:3.14-slim-trixie+ rustup-pinned toolchain) configures--oteland buildsfake_otlp, so the pytest path exercises the otel staticlib +test_otel.py.pkg/docker/local/Dockerfile.{minimal,php8.5,wasm}(run viapkg/docker/build-local.sh -b minimal) buildFROM ghcr.io/freeunitorg/freeunit-builder:trixie-rust1.94.1— a pre-built builder image with Rust already baked in — and already configure--otel(--njs --otel --zlib --zstd --brotli). NOTE:pkg/docker/local/is experimental — a local build-speedup only. It depends on the builder image being built first (no apt/rustup at build time), is not wired into CI, and is not the canonical/release image set (that ispkg/docker/Dockerfile.*). Treat it as a fast dev loop, not the documented smoke-test of record.
Remaining gaps:
-
pkg/docker/local/Dockerfile.*hardcodegit clone -b 1.35.5andLABEL ... version="1.35.5"— bump on each release (for local branch testing, override the clone ref per the CLAUDE.md note). - Decide the fate of the experimental
pkg/docker/local/set: promote it to a documented/CI fast-path, or keep it as a personal dev shortcut. Until then, do not point users' "rebuild Docker images locally" flow at it as the official smoke-test. - The CLAUDE.md "rebuild Docker images locally" snippet documents a
php:8.5-cli-trixiebuild without--otel— at minimum mention the--otelflag there so the documented smoke-test can cover otel (it needs a Rust toolchain in that image, which the php base lacks). - Once the
rust1.xvariant lands itsrust:1-slim-trixiebase, fold the builder-image andrust:1-slim-trixieapproaches into one documented story (both already ship Rust; no rustup step needed).
https://github.com/avahahn/ngx-testing-fmk — personal shell-based framework by Ava Hahn
(ex-F5, co-author of FreeUnit OTel layer; new account: @ava-affine, ava@sunnypup.io) for running nginx/nginx-otel tests across multiple
libvirt VMs in parallel.
What it does: boots libvirt VMs, rsyncs source + test dirs, builds and runs tests
on each VM in parallel, collects logs, shuts VMs down. common.sh provides a reusable
parallel_invoke_and_wait bash helper (fan-out with aggregated exit codes).
Why it is not a direct fit for FreeUnit:
- Hardwired to nginx + nginx-tests + nginx-otel — no Unit/FreeUnit hooks
- Requires a pre-configured libvirt infrastructure with shared credentials (
SECRET.sh) - FreeUnit already has pytest (
test/) + GitHub Actions CI covering the same ground - 0 stars, last commit Jan 2025, no active maintenance
What is worth studying:
parallel_invoke_and_waitpattern incommon.sh— clean bash fan-out with per-input log files and aggregated failure reporting; could inform a futuretest/run-matrix.shif we ever need to test across distros locally without Docker- Overall VM lifecycle approach (on → sync → build → test → off) as a template if we add libvirt/QEMU-based cross-distro testing outside of GitHub Actions
Decision: no code to borrow now. Revisit if we add a local multi-distro test matrix.
https://github.com/nginx/ngx-rust — Rust bindings for nginx dynamic modules by F5/NGINX.
Longevity risk: project is F5-controlled, WIP, and 60% of commits come from a single
engineer (bavshin-f5). F5 archived nginx/unit in Oct 2025 — same pattern applies here.
Use as a reference only; do not take a hard dependency.
ngx-rust ≠ Rust runtime support. ngx-rust is about writing nginx modules in Rust. For FreeUnit there are two separate ideas worth separating:
Study nginx-sys FFI layer and build.rs/bindgen approach; apply safe/unsafe separation
patterns to src/otel/.
- Clone ngx-rust, study
nginx-sys(FFI) andbuild.rs(bindgen) - Apply safe wrapper patterns to
src/otel/
Current state: no native Rust runtime exists.
- Go has
go/library (libunit-go, 676 lines) — apps import it, it linkslibnxt_unit.aand speaks the Unit app-worker protocol via Unix sockets. - Rust apps today: only path is compile to
wasm32-wasi→ run via FreeUnit wasm runtime.
Path A — bindgen + FFI (recommended, ~2–4 weeks)
- Run
bindgenonnxt_unit.h→ generate Rust FFI types - Write safe wrappers (~500–1000 lines), mirroring
go/unit.go,go/port.go,go/request.go,go/response.go - Add
axum/hyperadapter so users drop inlibunit-rustlike they dolibunit-go - Pro: reuses existing 6800-line
nxt_unit.c, protocol already battle-tested - Con: C linkage required (
libnxt_unit.a), same as Go
Path B — pure Rust protocol reimplementation (~2–3 months, high risk)
- Reverse-engineer Unix socket framing from
nxt_unit.c(6800 lines) - Pro: no C dependency, fully async-native (tokio)
- Con: high risk of protocol bugs, large effort
Recommendation: Path A first. Path B only if libunit-rust gains traction and
users demand a zero-C dependency.
- Prototype
libunit-rustvia Path A (reference:go/*.go,src/nxt_unit.h) - Decide: native
libunit-rustvs WASM-first (WASM works today, lower barrier)
All trixie-based Dockerfiles already install Rust at build time (for wasmtime /
wasm-wasi-component) and then discard it. Go variants keep Go in the final image
(FROM golang:1.24-trixie). Same pattern applies for Rust:
Proposed Dockerfile.rust1.x:
- Base:
FROM rust:1-slim-trixie(official Rust image, trixie variant) - Add
wasm32-wasip1target:rustup target add wasm32-wasip1 - Build FreeUnit with wasm + wasm-wasi-component modules (same as
Dockerfile.wasm) - Keep Rust toolchain in final image — users compile and serve Rust WASM in one container
Value: "compile and run Rust WASM apps with a single FreeUnit container" — no separate build step, no external toolchain. Works today via existing wasm runtime.
- Check
rust:1-slim-trixieexists on Docker Hub and is suitable as base - Add
Dockerfile.rust1.xtopkg/docker/andrelease-docker.ymlmatrix - Add
rust1.xtobuild-local.shALL_VARIANTS
Branch pre-1.35.5-i58 implements automatic chunked → Content-Length conversion
for proxy request forwarding. Key files:
src/nxt_h1proto.c— buffer fix (L1149-1171) + CL injection (L2414-2475)test/test_proxy_chunked.py— 10 tests (all passing)test/fake_upstream/— Rust HTTP mock with strict CL validation
Tests: 10/10 passed ✅ clang-ast: PASSED ✅
Pending upstream:
- Consider making the conversion configurable (currently always-on when
r->chunked) - Add metrics/counter for chunked → CL conversions
- Consider adding
Transfer-Encodingremoval for HTTP/2 upstream (HTTP/2 doesn't use TE header)
Backend returns 411 when FreeUnit forwards a chunked POST with no Content-Length.
Workaround today: client-side buffering (git config http.postBuffer).
Design questions to resolve before implementation:
- Where does
request_bufferinglive — on theproxyaction object orsettings.http? Per-action is more composable (can disable for upload routes); global is simpler but can't be selectively disabled. - After buffering: does FreeUnit strip
Transfer-Encoding: chunkedand injectContent-Length, or re-encode? Must define behavior before writing the code.
Implementation risks:
- 🟡 Memory:
max_body_sizecan be up to 17 GB. Need a per-request memory cap and a disk spill path — not just a flag that buffers everything in-process.
Related upstream nginx/unit issues: #445, #1088, #1278