Fixed
- Bounded static asset path validation to a fixed number of residual
percent-decoding passes. A path such as/static/%2525…2541previously drove
a decode loop that shrank by two bytes per pass, costing time quadratic in
the request path length; a 16 KiB path burned tens of milliseconds of CPU per
unauthenticatedGETand still resolved successfully. Chains longer than the
budget are now rejected as residual percent-encoding. - Removed the generated route-list cache. It was keyed by
TypeId, which
identifies behavior only for types with a single inhabitant, so two different
applications coerced to the samefn() -> _pointer type shared one cached
list and the second served the first's routes. That unsoundness was first
narrowed to zero-sized closures, but the cache could never pay for itself:
both supported hosts instantiate a fresh component per request, so a
thread-local map starts empty on every lookup and is discarded with the
instance. It only added a map allocation and a full deep clone of the route
list to each request. Route discovery is now unconditional and correct by
construction. - Accepted a
v-prefixed version when resolving a pinned tool. The whole-version
rule required a non-version character immediately before the digits, and
cosign versionreportsGitVersion: v3.1.1and nothing else, so no cosign
build that has ever shipped could satisfy it. Every gate that verifies a
signature was unreachable as a result: both component sync scripts and, through
them, the authorization browser lane failed at tool resolution withcosign 3.1.1 is requiredwhile the correct cosign was onPATH. The rule is still a
whole-version test —4.0.2continues to reject a reported4.0.21, and
3.1.1rejectsv13.1.1.
Changed
-
Split
handler.rs(3,313 lines) intosrc/handler/andexecutor.rsinto
src/executor/. The handler file held seven unrelated concerns plus both
host adapters; it is now a 37-line module root over eleven submodules, and
the largest file in the crate is 1,026 lines. This is code movement only:
the public API is unchanged (cargo-semver-checks: 223 pass, no update
required),lib.rsandrequest.rsare untouched, and the lib test set is
identical test-for-test. Items shared between submodules arepub(super),
which is the access they already had while everything lived in one file. -
Enabled
clippy::pedanticand moved the lint configuration into
Cargo.toml's[lints]tables.forbid(unsafe_code)and
deny(missing_docs)now cover tests, benches, and examples rather than the
library target alone, and a plaincargo clippyreports exactly what CI
enforces instead of the weaker default set.clippy::unwrap_usedand
clippy::panicare denied: both were already absent from production code,
and a panic in a WASI component traps the instance rather than unwinding. -
Sixteen public fallible APIs now document how they fail. Both
Handler
constructors, bothhandle_with_context, the five route generators,
static_files_handler,from_wasi_request, the two WASI-to-HTTP
converters, andResponse::headersgained# Errorssections naming the
variants they return. -
render_modetakes&SsrModerather than an owned value, dropping a clone
on every SSR render, and the Preview 2 initializer no longer clones an
executor it drops immediately afterwards. -
Route discovery no longer runs on requests that cannot use the SSR router. A
server function, a static asset, and an already-selected response all resolve
without it, but discovery renders the whole application to extract its
<Routes/>— measured at 183 µs of a 1054 µs request, and paid on every
request because both supported hosts instantiate a fresh component each time.
0.3.2 skipped this and 0.4.0 stopped doing so; the results were generated and
then discarded. Restoring the skip recovers 7.35pp on server-function and
static-asset requests (13.13% → 5.78% slower than 0.3.2, paired, n=5), and
leaves requests that do consult the router unchanged, as expected.This trades away a validation step: an application reached only through
server functions and static assets no longer rejects a malformed route table
at request time.leptos_wasi::validate_route_tableis the replacement — call
it once from a test to apply exactly the rules the request path applies. -
Every soak lane now compares against a baseline instead of only an absolute
ceiling. The WASIp3 lane had no baseline at all — WASIp3 postdates 0.3.2, so
there was nothing historical to compare it to — and its 150 ms ceiling left
60 ms of headroom above the 88 ms observed, meaning a gradual regression
could not have been detected there. Each lane now baselines at the oldest
commit that supports its preview: WASIp2 at 0.3.2, WASIp3 at the final WASI
0.3 migration. The absolute ceiling additionally runs on every lane, because
a relative gate cannot see a slowdown that was already present when its own
baseline was recorded. -
Soak latency and throughput budgets are set per lane rather than globally.
The WASIp2 lanes span the 0.4 adapter rewrite and carry a measured, documented
per-request cost, so their budgets clear it and catch anything worse; the
WASIp3 baseline already contains that cost and keeps the tight defaults. See
PERFORMANCE.mdfor the measurements behind each number. -
Every response the crate emits now carries
X-Content-Type-Options: nosniff,
not only static assets. The crate's own error bodies — 404, the static 405,
and the 413/400 request-policy rejections — additionally declare
text/plain; charset=utf-8; they previously carried no content type at all,
which is the case content sniffing exploits. The default is applied once, at
the end of the shared render path, and only when the header is absent, so an
application that sets its own value throughResponseOptionsstill wins. The
WASIp2 internal-error response is constructed outside that path and sets both
headers itself. This also makes the 0.4.0 note about static routes adding
X-Content-Type-Optionstrue of the 405 response, which previously carried
onlyAllow. -
Advanced the companion pin from
wasi-auth 0.1.0-alpha.4to0.1.0-rc.3,
pinned at the commit its release tag points at rather than at the branch tip.
Every symbol the fixtures import is unchanged across the range — upstream
reports no.rsfile touched in any consumer-visible crate — so the move is
version metadata, regenerated artifact records, and lockfiles, with no code
change on either side.wasi-http-authnstays at0.2.0-alpha.3, because
the legacy middleware subtree is byte-identical across the same range.The authorization artifact set is now recorded from the assets published on
the upstream release for that tag, not from a local pipeline run. Upstream
signs with a key generated per run and discarded, so the signature and key
digests a local rerun produces can never match a previously recorded set;
one immutable published bundle is the only thing those fields can pin. This
also drops two revisions the lock could not resolve: anartifact_revision
absent fromwasi-authhistory, and a[middleware].baseline_revisionthat
belongs to the standalonewasi-http-middlewarehistory and is now recorded
with that provenance instead of presented as awasi-authcommit. -
Pinned the companion surface by digest. The lock named two crates while the
fixtures compile six, and the four beyond those must still exist in the
checkout for a--lockedbuild, so a rename or a version bump upstream
reached this repository as a resolution failure inside a fixture build.
[authorization]now records the companion's owncompanion.toml— which
upstream generates fromcargo metadataacross both its workspaces and
drift-checks in its CI — andcheck-authz-companion.shverifies that digest
before it builds anything.The digest alone would only report that something moved, so the same check
then names what moved: every directly usable crate must sit on its own
workspace's locked version line, the legacy workspace included on its
independent one, and the recorded artifact identity and component list must
agree with the lock. A bumped crate now fails ascompanion crate wasi-authz-cedar is 0.1.0-rc.9, but wasi-auth is locked at 0.1.0-rc.3. -
Recorded the
wasi-http-middlewareartifact set from a published release
instead of from a local build. Its digests previously came from whoever last
ran the upstream supply-chain script on their own machine, naming files that
exist in no checkout —artifacts/andreports/supply-chain/are
git-ignored upstream — so the signed-artifact gate for this bundle could not
run at all. Upstream now cuts it from a tag, and the digests here come from
those assets. The component digests moved, because they embed absolute source
paths and so depend on where the build ran; the SBOM and WIT digests did not
move at all, which is what a reproducible record should look like.This also corrects a claim made when the authorization pin advanced.
[middleware].baseline_revisionwas described as resolvable only in the
standalonewasi-http-middlewarehistory and never inwasi-auth's. It is
reachable fromwasi-auth'smain: the subtree import preserved that
history rather than squashing it, and the same is true of the middleware
source_revisionandartifact_revision. The one revision that genuinely
does not sit on mainline is the authorizationartifact_revisionthis cycle
already replaced, which is on a diverged branch. -
scripts/verify-artifact-set.pycompares the authorization bundle's identity
against the lock'sartifact_name/artifact_versionrather than its
name/version. Those pairs deliberately differ — thewasi-auth
repository ships awasi-authzbundle — and the static manifest audit
already required theartifact_*keys, so the signed-artifact gate could not
pass at the same time as the audit. That was true of the records shipped
before this change, not a consequence of it; the gate is satisfiable for the
first time here. A lock section withoutartifact_*keys, such as the
middleware one, still matches onname/version.
Added
-
cargo make benchruns a host-native route-discovery benchmark over 3, 8,
and 32 routes. Route discovery is the largest in-guest cost of a request
that renders — roughly 183 µs of 1054 µs — and most of it happens inside
leptos_router::RouteList::generate, which this crate does not own. A
leptos bump that regressed discovery by 40% would move an end-to-end request
by about 7%, inside the soak job's 8–12% budget, so nothing in CI would see
it. Deliberately not wired into push/PR CI: the soak job's own notes record
a ±0.7pp measurement problem on shared runners over 600 seconds, which a
microsecond-scale sample cannot survive. -
The public-API fixtures in
tests/api-fixtures/exercise every builder
method. All three previously called onlybuild/build_with_config, and
four of the seven registration methods had no call site anywhere in the
repository, so the one CI job whose purpose is compiling external-consumer
imports could not have caught a change to them. -
HandlerConfig::with_request_body_timeout_nsbounds how long a whole request
body may take to arrive, andrequest_body_timeout_nsreads it back. It is
Noneby default, so no existing deployment changes behaviour: request
deadlines remain a deployment responsibility, and this is defense in depth
for an ingress that cannot supply one. Exceeding the budget produces
408 Request Timeout. Preview 2 polls the input stream against a monotonic
timer instead of blocking on it, and Preview 3 races the length-limited
collect against the same duration, so both previews apply one whole-body
budget rather than a per-chunk one. -
validate_route_table, which checks an application's route table without
serving a request. Route discovery is skipped on requests that cannot use the
SSR router, so a route table reached only through server functions and static
assets is never validated in production; calling this once from a test closes
that gap. It shares its implementation with the request path, so the rules a
deployment enforces and the rules a test suite checks cannot drift apart. -
Unit coverage for the previously untested request, response, redirect, and
executor surfaces: WASI Preview 2 method and scheme conversion,Accept
negotiation, referrer andLocationsanitization,utils::redirect,
axum_coreand boxed-error body streaming,ResponseOptionsapplication
semantics, pollable dispatch through an injected poller, and both executor
scheduling modes. -
An end-to-end regression asserting that a nested percent-encoding chain is
rejected without a latency blow-up relative to a plain 404. -
Coverage for
Locationhandling through the whole render path, rather than
only throughapply_server_fn_redirectin isolation: aLocationwritten
throughResponseOptionsreaches the client unchanged, including an
off-origin scheme and authority, while aLocationset on a server
function's own response is reduced to a same-origin path. The end-to-end
open-redirect assertions now require an exact same-origin path instead of a
matching suffix, which an absolute URL could satisfy. -
A
companion-authzCI job, so the authorization companion gates run on every
push instead of only from a maintainer's checkout. It reads the pinned
revision out oftests/middleware/components.lock.toml, checkswasi-auth
out beside this repository at that revision — the layout both the fixtures'
relative path dependencies andauthz_repositoryrequire — installs the
bundle published for the matching tag, and runs
sync-authz-components.sh: companion revision and version, baseline
ancestry, a clean companion tree, locked fixture clippy, every digest in the
artifact set, bothcosign verify-blobcalls, and per-component
wasm-tools validatewith the WIT stack-order assertions. It then builds the
WASIp2 lifecycle fixture--lockedagainst the same companion.Advancing the pin stays a lock edit: the workflow names no revision, version,
or tag of its own. The bundle is fetched from the release rather than rebuilt
because the upstream signing key is ephemeral per run, so a rebuild could
never reproduce the recorded signature digests.The same job also verifies the
wasi-http-middlewarebundle, on its own
version line and its own release tag, so both signed artifact sets are now
enforced on every push and no gate is left claiming it cannot run.
Documentation
- Documented
validate_route_tableandwith_request_body_timeout_nsin the
README and the production contract. Both shipped in this cycle without
reaching either document, andvalidate_route_tablein particular is the
mitigation for route-table validation no longer running on claimed requests,
so its absence from the operational contract was the more consequential gap. - Rewrote the companion-pin sections of the README and
MIDDLEWARE.md. They
described the pin as deliberately held behind upstream and the authorization
bundle as an unregenerated pre-consolidation set, which the pin move in this
same cycle made false. They now state where the bundle comes from and why it
cannot be reproduced locally, which is the part a reader needs in order to
re-verify it. - Reorganised the README's runtime section, which had grown into a single
paragraph covering composition, Spin canaries, companion components, and
post-0.5 scope. It is now split by subject, with the per-runtime composition
status as a table. - Documented component instance reuse, which is the largest throughput lever
available to a WASIp2 deployment and is not controlled by this crate.
wasmtime serve --max-instance-reuse-countdefaults to 1 for WASIp2 and 128
for WASIp3, so a Preview 2 deployment rebuilds the component for every
request unless it opts out: measured at roughly 107 µs per request, about 14%
of throughput at low concurrency, which exceeds any single change this crate
has made to its own request path. The pooling allocator was measured
alongside it and is indistinguishable from noise, so it is not recommended.
Reuse makes guest statics outlive a request, soPRODUCTION.mdrecords both
the win and the obligation to test for it;LEPTOS_WASI_MAX_INSTANCE_REUSE
runs this repository's e2e suite under reuse, where both previews pass. - Corrected
PERFORMANCE.md, which presented 779 µs of a 1054 µs request as
though instantiation dominated it. Instantiation is roughly 107 µs; the rest
is the host HTTP path, the socket, and the probe's own client overhead. An
earlier revision used the larger figure to argue that module size drove the
regression, which the arithmetic never supported. - Documented the per-request Leptos nonce the handler already provides and a
recipe for emitting aContent-Security-Policyfrom the application through
ResponseOptions, including the'wasm-unsafe-eval'requirement for
hydration and the fact thatleptos_meta'sStylesheetandLink
components accept no nonce attribute. - Recorded that guest-served static responses carry no
Cache-Control,
ETag, orLast-Modifiedand answer no conditional request with304, and
that the Spin manifests obtainCache-Controlfromspin-fileserverrather
than from this crate. - Separated body size, which is bounded on both previews, from body read time,
which is bounded on neither. The host supplies the request-read/idle deadline
and the concurrent-instance cap in addition to a size limit. - Documented that a server-function response has its
Locationreduced to a
same-origin path, that aLocationwritten throughResponseOptions
bypasses that reduction, and that both differ fromleptos_axumand
leptos_actix.