Skip to content

Releases: numerum-tech/rustf

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 13:26

First stable release. Changes since 1.0.0-rc3:

Fixed

  • A project generated by rustf-cli new project depends on the published
    framework.
    The scaffold wrote rustf = { 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 use rustf = "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.sh runs 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

v1.0.0-rc3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 23 Oct 12:35

Added

  • Configuration values from the environment or from files. Any string
    value in config.toml or 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_bool and
    AppConfig::section parse 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 validate now loads the effective
    configuration and checks the references of config.prod.toml.
    Templates can read CONF, including referenced values: do not output
    secrets there.

Fixed

  • The session cookie's Secure flag followed the wrong variables. It was
    decided from RUSTF_ENVIRONMENT, RUST_ENV or NODE_ENV — never from
    RUSTF_ENV or [app] environment — so an application configured for
    production could issue non-Secure cookies (any build with
    NODE_ENV=development, or a debug build with RUSTF_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, and RUSTF_ENV=prod made a worker's is_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, secure and http_only, none of which is read:
    session IDs are random and stored server-side (there is no signing secret),
    the timeouts are idle_timeout / absolute_timeout, and cookies are always
    HttpOnly and Secure in production. Changing timeout had no effect. The
    scaffold now writes the real keys; existing projects should rename timeout
    to idle_timeout and 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 Err now answers with the error's own status instead of
    a blanket 500.
    ctx.require_auth()? is a 401, ctx.param_int("id")? on
    /posts/abc is a 400, and the rest of Error::status_code()'s mapping
    (403, 404, 429) applies too. Previously every failing handler flattened
    to 500 Internal Server Error, which made the idiomatic guard unusable and
    left the 401/403/404/429 arms of Error::status_code() as dead code.
    The mapping also covers controller before hooks 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 a 5xx never discloses
    its own message — Error::client_message() returns the error text only for
    Validation, InvalidInput, Authentication and Authorization, and
    everything else (including anything added with with_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
    500 on a route that returns Err needs updating.

  • Error::Timeout maps to 504 Gateway Timeout, not 408 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 a 500. ctx.body_json() /
    Request::body_as_json() reported a parse failure as Error::Internal; it is
    now Error::InvalidInput, the same as a form body that does not deserialize.
    The client reads only Invalid 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 a 400, but its body
    is now Invalid form data instead 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 returned Err or InboundAction::Stop, only the outbound
    middleware it had already passed in priority order ran over the response. An
    auth middleware at priority -100 therefore sent its 401 — and a rate
    limiter at -900 its 429 — without SecurityHeadersMiddleware (priority
    0). Outbound-only middleware further down the list now runs too; dual-phase
    middleware whose inbound phase never ran is still skipped.

  • A SOCKET route on an app with no middleware at all now upgrades. The
    zero-middleware fast path in execute_middleware_chain returned before
    start_pending_upgrade ran, so such an app answered the handshake with 101
    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 an Accept header that ranks a JSON type (application/json
    or any +json suffix, such as application/problem+json) at least as high as
    text/html (q-values respected). When Accept names neither, a classic XHR
    (X-Requested-With: XMLHttpRequest) counts as JSON; htmx does not — even
    with X-Requested-With added 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 from Error::status_code(), body from
    Error::client_message(), as plain text or as
    {"error": ..., "code": ...}. A 5xx reports "code": "E_INTERNAL"
    whichever variant failed.
  • Error::client_message() — the text an error may safely show the client.

Changed

  • RUSTF_ENV is the only environment variable read. RAILS_ENV and
    NODE_ENV (documented) and RUSTF_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. Set RUSTF_ENV or
    [app] environment instead.
  • AppConfig::detect_environment() returns Result<Environment>, and the
    new Environment::parse rejects unknown names. Environment::from_str, which
    maps unknown names to development, is deprecated.
  • From<config::SessionConfig> for the session manager's SessionConfig is
    removed; use SessionConfig::from_app_config(&app_config).
    The session
    section alone cannot tell whether the app runs in production, so the
    conversion set the cookie's Secure flag from a guess. Code that built a
    custom SessionMiddleware with app_config.session.clone().into() now fails
    to compile instead of silently issuing non-Secure cookies in production.
  • RustF::new() and WorkerManager::new() follow RUSTF_ENV (they load
    no configuration file). An invalid value is logged and treated as
    production, the safe side.
  • DEBUG is no longer a Lazy<bool> but a Debug value that reads the
    current environment on every access (*DEBUG and DEBUG.get()), so it
    cannot keep a value computed before the configuration was loaded.
  • routes! no longer accepts SOCKET as a custom HTTP method. The macro's
    catch-all arm used to stringify any identifier, so routes![SOCKET "/x" => h]
    registered a route whose method was literally SOCKET. It now builds a
    WebSocket route: the entry registers as GET, a plain GET gets 426, and
    with --no-default-features (no websocket feature) the invocation does not
    compile. Anything genuinely dispatching a custom SOCKET method must build
    the route with Route::new("SOCKET", path, handler) instead.
  • TrieRouter::add_route takes a RouteInfo instead of five positional
    arguments. Router::add_route(Route) — what applications and controllers
    use — is unchanged.

Added

  • WebSocket support, behind the default-on websocket feature. Total.js
    shaped: routes![SOCKET "/chat/{room}" => chat] registers a GET route that
    only accepts a handshake — a plain GET gets 426 Upgrade Required and the
    handler never runs. ctx.socket(|room| ..) configures the room for the
    matched URL for the first client, with on_open / on_message /
    on_close / on_error closures; 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...
Read more