Repository navigation
Releases: numerum-tech/rustf
Release list
v1.0.0
First stable release. Changes since 1.0.0-rc3:
Fixed
- A project generated by
rustf-cli new projectdepends on the published
framework. The scaffold wroterustf = { path = "../rustf" }, which only
resolves inside the framework's own repository: a project generated by a
CLI installed from crates.io did not build. It now writes
rustf = "<the CLI's version>"; the local-path form remains as a comment.
The installation guides userustf = "1.0"too. - Builds without warnings on Rust 1.99.
AtomicUsize::fetch_update, used
when a WebSocket connection permit is released, is deprecated there; it is
replaced by a compare-exchange loop that compiles warning-free on older and
newer toolchains alike.
Added
scripts/ci-local.shruns what CI runs (per crate, a warnings-denied
build with all features, then the tests), to catch breakage before a push.
v1.0.0-rc3
Added
- Configuration values from the environment or from files. Any string
value inconfig.tomlor an overlay can be"(env) NAME"(an environment
variable) or"(file) PATH"(a file's contents, e.g. a Docker/Kubernetes
secret under/run/secrets/; relative paths are relative to the config
file). This works for every key, including custom sections, and for
rustf-cli. A missing variable or file, or an unknown tag such as
"(evn) PORT", stops the application at startup with an error that names
the key; resolved values are masked in loading errors (always where quoted,
and unquoted from 8 characters).(file)reads
regular files of at most 1 MiB. Framework keys take their declared type
("(env) PORT"becomes an integer); everything else stays a string, and
CONF::get/get_or/get_int/get_float/get_booland
AppConfig::sectionparse such strings when the requested type is a number
or a boolean. A value that must literally start with a marker doubles the
parenthesis:"((env) text".rustf-cli validatenow loads the effective
configuration and checks the references ofconfig.prod.toml.
Templates can readCONF, including referenced values: do not output
secrets there.
Fixed
-
The session cookie's
Secureflag followed the wrong variables. It was
decided fromRUSTF_ENVIRONMENT,RUST_ENVorNODE_ENV— never from
RUSTF_ENVor[app] environment— so an application configured for
production could issue non-Securecookies (any build with
NODE_ENV=development, or a debug build withRUSTF_ENV=production). It now
follows the application's configured environment. -
An unknown environment name no longer runs as development.
RUSTF_ENV=staging,[app] environment = "staging"or a typo silently
selected development — dev overlay, no production security defaults,
detailed error pages. Loading now fails with an error naming the value. -
Workers and events saw a different environment than the application.
They read their own variables (NODE_ENV,RUST_ENV,APP_ENV), ignored
[app] environment, andRUSTF_ENV=prodmade a worker'sis_production()
false. Both now report the configured environment ("development"or
"production"); detailed error pages follow it too, instead of the build
profile. -
The generated
[session]configuration no longer contains keys the
framework ignores.rustf-cli new project(and the sample app) wrote
secret,timeout,secureandhttp_only, none of which is read:
session IDs are random and stored server-side (there is no signing secret),
the timeouts areidle_timeout/absolute_timeout, and cookies are always
HttpOnlyandSecurein production. Changingtimeouthad no effect. The
scaffold now writes the real keys; existing projects should renametimeout
toidle_timeoutand may delete the other three. -
Configuration docs described environment variables that are not read
(RUSTF_SESSION_*,RUSTF_CUSTOM_*, "all values can be overridden with
RUSTF_") and a"${VAR}"syntax that loaded as literal text. They now list
the variables actually read and document"(env) NAME". -
A handler returning
Errnow answers with the error's own status instead of
a blanket500.ctx.require_auth()?is a401,ctx.param_int("id")?on
/posts/abcis a400, and the rest ofError::status_code()'s mapping
(403, 404, 429) applies too. Previously every failing handler flattened
to500 Internal Server Error, which made the idiomatic guard unusable and
left the 401/403/404/429 arms ofError::status_code()as dead code.
The mapping also covers controllerbeforehooks and middleware errors.It runs before the outbound middleware stack, so security headers, CORS and
compression apply to the error response; a response the handler had already
written is discarded in favour of the error's; and a5xxnever discloses
its own message —Error::client_message()returns the error text only for
Validation,InvalidInput,AuthenticationandAuthorization, and
everything else (including anything added withwith_context) collapses to
the canonical reason phrase, with the full chain going to the error log.This changes the status code of every failing handler. Anything asserting
500on a route that returnsErrneeds updating. -
Error::Timeoutmaps to504 Gateway Timeout, not408 Request Timeout.
The framework raises it when a dependency does not answer in time (a worker
run with a timeout, for instance), which is the server's failure, not a slow
client. As a 5xx it is now logged as an error and its message is masked. -
A malformed JSON body is a
400, not a500.ctx.body_json()/
Request::body_as_json()reported a parse failure asError::Internal; it is
nowError::InvalidInput, the same as a form body that does not deserialize.
The client reads onlyInvalid JSON body; the parser's diagnostic, which can
name fields of the server-side type, is logged at debug. -
ctx.body_form_typed()no longer echoes the deserializer's diagnostic to
the client. A form that does not deserialize is still a400, but its body
is nowInvalid form datainstead of serde's text (which named the struct's
fields, their Rust types and enum variants). The diagnostic is logged at
debug. Any client that parsed that message needs updating. -
An early inbound exit still gets every outbound-only middleware. When an
inbound middleware returnedErrorInboundAction::Stop, only the outbound
middleware it had already passed in priority order ran over the response. An
auth middleware at priority-100therefore sent its401— and a rate
limiter at-900its429— withoutSecurityHeadersMiddleware(priority
0). Outbound-only middleware further down the list now runs too; dual-phase
middleware whose inbound phase never ran is still skipped. -
A
SOCKETroute on an app with no middleware at all now upgrades. The
zero-middleware fast path inexecute_middleware_chainreturned before
start_pending_upgraderan, so such an app answered the handshake with101
and then never upgraded. Every builtin middleware is opt-in and the session
middleware is only registered when sessions are enabled, so the path was
reachable.
Added
Request::wants_json()— whether the client would rather have JSON than
HTML: true for anAcceptheader that ranks a JSON type (application/json
or any+jsonsuffix, such asapplication/problem+json) at least as high as
text/html(q-values respected). WhenAcceptnames neither, a classic XHR
(X-Requested-With: XMLHttpRequest) counts as JSON; htmx does not — even
withX-Requested-Withadded by an interceptor — since it swaps the response
into the page.Response::from_error(&Error, json: bool)— builds the response for a
framework error: status fromError::status_code(), body from
Error::client_message(), as plain text or as
{"error": ..., "code": ...}. A5xxreports"code": "E_INTERNAL"
whichever variant failed.Error::client_message()— the text an error may safely show the client.
Changed
RUSTF_ENVis the only environment variable read.RAILS_ENVand
NODE_ENV(documented) andRUSTF_ENVIRONMENT,RUST_ENV,APP_ENV
(undocumented, read by some components) are ignored: a container that sets
them for other tools no longer changes RustF's mode. SetRUSTF_ENVor
[app] environmentinstead.AppConfig::detect_environment()returnsResult<Environment>, and the
newEnvironment::parserejects unknown names.Environment::from_str, which
maps unknown names to development, is deprecated.From<config::SessionConfig>for the session manager'sSessionConfigis
removed; useSessionConfig::from_app_config(&app_config). The session
section alone cannot tell whether the app runs in production, so the
conversion set the cookie'sSecureflag from a guess. Code that built a
customSessionMiddlewarewithapp_config.session.clone().into()now fails
to compile instead of silently issuing non-Securecookies in production.RustF::new()andWorkerManager::new()followRUSTF_ENV(they load
no configuration file). An invalid value is logged and treated as
production, the safe side.DEBUGis no longer aLazy<bool>but aDebugvalue that reads the
current environment on every access (*DEBUGandDEBUG.get()), so it
cannot keep a value computed before the configuration was loaded.routes!no longer acceptsSOCKETas a custom HTTP method. The macro's
catch-all arm used to stringify any identifier, soroutes![SOCKET "/x" => h]
registered a route whose method was literallySOCKET. It now builds a
WebSocket route: the entry registers asGET, a plainGETgets426, and
with--no-default-features(nowebsocketfeature) the invocation does not
compile. Anything genuinely dispatching a customSOCKETmethod must build
the route withRoute::new("SOCKET", path, handler)instead.TrieRouter::add_routetakes aRouteInfoinstead of five positional
arguments.Router::add_route(Route)— what applications and controllers
use — is unchanged.
Added
- WebSocket support, behind the default-on
websocketfeature. Total.js
shaped:routes![SOCKET "/chat/{room}" => chat]registers aGETroute that
only accepts a handshake — a plainGETgets426 Upgrade Requiredand the
handler never runs.ctx.socket(|room| ..)configures the room for the
matched URL for the first client, withon_open/on_message/
on_close/on_errorclosures; everyone after joins it. (Once per room,
not once per process: an autodestroyed room — or one whose closure panicked —
is configured again by the next conn...