docs-site/content/docs/extras/upgrades.md
+++ title = "Upgrades" description = "" date = 2021-05-01T18:20:00+00:00 updated = 2021-05-01T18:20:00+00:00 draft = false weight = 4 sort_by = "weight" template = "docs/page.html"
[extra] lead = "" toc = true top = false flair =[] +++
Cargo.tomlcargo loco doctor inside your project to verify that your app and environment is compatible with the new versionAs always, if anything turns wrong, open an issue and ask for help.
Loco is built on top of great libraries. It's wise to be mindful of their versions in new releases of Loco, and their individual changelogs.
These are the major ones:
1.0 is a large, intentionally-breaking release — the first stable Loco. Its headline change is the move to Sea-ORM 2.0. This section is assembled per area; start with the Sea-ORM steps, which affect every app that uses a database. Cross-check the 1.0.0 CHANGELOG for anything specific to APIs you use directly.
Loco 1.0 uses Sea-ORM 2.0, whose MSRV is Rust 1.94. Update your toolchain:
rustup update
Loco upgraded from Sea-ORM 1.1 to Sea-ORM 2.0. For most apps the migration is
mechanical — bump the pins and the CLI — because Loco's schema helpers and the
generated model/migration shapes absorb the API changes for you.
1. Bump the dependency pins. In your app Cargo.toml:
# before
sea-orm = { version = "1.1", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] }
# after
sea-orm = { version = "2.0", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] }
And in your migration/Cargo.toml:
# before
sea-orm-migration = { version = "1.1.0", features = [...] }
# after
sea-orm-migration = { version = "2.0", features = [...] }
If you depend on sqlx directly, bump it to 0.9.
2. Update the Sea-ORM CLI (used by cargo loco db entities) to 2.0:
cargo install sea-orm-cli --version '^2.0'
cargo loco doctor will now flag a Sea-ORM or Sea-ORM CLI older than 2.0.
3. Regenerate entities (recommended). Run cargo loco db entities so your
src/models/_entities/ are produced by the 2.0 codegen.
4. Hand-written queries / migrations. If you wrote custom raw SQL or custom migrations, apply these Sea-ORM 2.0 changes (the same ones Loco itself made):
Statement calls gain a _raw suffix. db.execute(stmt) →
db.execute_raw(stmt); db.query_one(stmt) / db.query_all(stmt) →
query_one_raw / query_all_raw. SeaQuery statements (e.g. from
Entity::find().into_query()) are now passed by reference and need no
manual .build(...): db.query_all(&select).sqlx 0.9 requires runtime-built SQL strings to be wrapped in
AssertSqlSafe(...): sqlx::query(AssertSqlSafe(format!(...))).ExprTrait into scope for expression methods: use sea_orm::ExprTrait;.
Replace Alias::new("col") with the bare string "col".DbErr::BackendNotSupported { .. }
rather than panic; Sea-ORM 2.0 removed the internal panics (a new DbErr
variant carries the case). If you match on DbErr exhaustively, add the arm.insert_many no longer needs .on_empty_do_nothing(), and
exec_with_returning_many is now exec_with_returning.5. Note on Postgres auto-increment. Sea-ORM 2.0 emits
GENERATED BY DEFAULT AS IDENTITY instead of SERIAL for new
auto_increment() columns. Existing tables are unaffected; only newly generated
migrations differ. See the Sea-ORM 2.0 migration guide for the
option-postgres-use-serial escape hatch if you need the old behavior.
For the full upstream detail see the Sea-ORM 2.0 migration guide.
Newly generated models and scaffolds now use i64 (BIGINT) primary keys and
foreign keys, and the int/unsigned field types generate 64-bit columns. This
is required by Sea-ORM 2.0 (its codegen maps SQLite integers to i64) and
matches the modern bigint-by-default convention.
This only affects code you generate after upgrading — your existing tables,
migrations, and entities are untouched. If you scaffold new resources and want
them to relate to older i32-keyed tables, make the key types match (either
widen the old ones with a migration, or hand-edit the new id/foreign-key
fields back to i32).
ExtraDbInitializer → MultiDbInitializerThe single-extra-connection initializer (initializers.extra_db, which layered a
bare Extension<DatabaseConnection>) was removed. Use MultiDbInitializer with a
one-entry initializers.multi_db map instead, and extract the connection with
Extension<MultiDb>:
// before: Extension<DatabaseConnection>
// after:
let conn = multi_db.get("<name>")?;
Move whatever you configured under extra_db into a one-entry multi_db map.
AppContext is now #[non_exhaustive] — construct with the builderField access (ctx.db, ctx.config, State/FromRef extraction) is unchanged,
so most apps need no change. But direct struct-literal construction and exhaustive
pattern matches on AppContext from outside the framework no longer compile (this
makes future context fields non-breaking to add). If you built an AppContext by
hand — e.g. in a custom boot or test harness — use the builder:
let ctx = AppContext::builder(environment, db, config) // builder(environment, config) without `with-db`
.queue_provider(queue)
.mailer(mailer)
.storage(storage)
.build();
loco_rs::Error is now #[non_exhaustive]The framework's Error enum is marked #[non_exhaustive] so new variants can be
added in the future without a breaking change. If you match on loco_rs::Error
(or loco_rs::prelude::Error) exhaustively, add a wildcard arm:
match err {
Error::NotFound => { /* ... */ }
// ...handle the variants you care about...
_ => { /* fallback */ }
}
Most apps use Result<T> / ? and never match on Error directly, so no change
is needed.
IntoResponse for Error previously collapsed most variants to 500. Now
Model(EntityNotFound) → 404, Model(EntityAlreadyExists) → 409, and model
validation / form-body rejections → 4xx (matching JSON rejections); genuinely
internal errors still return 500. This is behavior-only — no API changed — but
if your tests asserted the old 500s, update them to the corrected codes.
Background jobs now support a priority (higher numbers run first). You can enqueue with an explicit priority:
DownloadWorker::perform_later_with_priority(&ctx, args, Some(42)).await?;
priority column is added to the
queue table automatically on startup; existing jobs default to priority 0.Mailer jobs enqueue at priority 100 by default; override per mailer via
MailerOpts { priority, .. }.
perform_later returns the job idWorker::perform_later now returns the enqueued job's id
(Result<String> instead of Result<()>), and Queue::enqueue returns
Result<Option<String>>. Existing call sites keep working — perform_later(..) .await?; simply ignores the returned id. Capture it when you want to track
status:
let job_id = DownloadWorker::perform_later(&ctx, args).await?;
QueueProvider adapterbgworker::Queue is now a newtype over Arc<dyn QueueProvider>, so backends are
pluggable. All queue methods keep the same signatures and behavior. Only two
source-level changes affect callers:
Queue::empty() instead of Queue::None.Queue::Postgres(pool, ..) to
reach the raw pool) no longer compiles — use the provider methods instead.PageResponse carries a meta: PagerMetaPagination results moved the flat total_pages / total_items fields into a
meta: PagerMeta (which also carries page and page_size):
// before
let total = page.total_pages;
// after
let total = page.meta.total_pages; // also: page.meta.page, page.meta.page_size, page.meta.total_items
MirrorStrategy / BackupStrategy → ReplicatedStrategyThe two strategies were the same primary-plus-secondaries replication engine and
are now one storage::strategies::replicated::ReplicatedStrategy with a single
FailurePolicy enum:
// MirrorStrategy::new(p, s, MirrorAll) ->
ReplicatedStrategy::mirror(p, s, FailurePolicy::FailIfAny);
// BackupStrategy::new(p, s, BackupAll) ->
ReplicatedStrategy::backup(p, s, FailurePolicy::FailIfAny);
Old FailureMode maps: AllowMirrorFailure / AllowBackupFailure → AllowAll,
AtLeastOneFailure → AllowSingleFailure, CountFailure(n) → FailAtFailures(n).
Former-backup secondary writes now run concurrently (were sequential); the
collected errors and failure decision are unchanged.
/ (security)storage::drivers::local::new() previously rooted the store at /, so a key
derived from user input could escape to the whole disk (key etc/passwd read
/etc/passwd). It now roots at the current working directory. If you relied on
absolute-path keys, opt back in explicitly:
local::new_with_prefix("/your/root")
{env}.local.yaml now deep-merges over {env}.yamlPreviously the first existing file won and the other was ignored, so a
.local.yaml had to restate the whole config. Both files now layer with local
precedence: mappings merge recursively; scalars and sequences in local replace the
base value (sequences are not concatenated). If you kept a full-config
.local.yaml, trim it to just the keys you override — base keys now persist
unless explicitly overridden.
404When the built-in fallback is enabled without an explicit code, it now returns
404 Not Found (matching its docs and the bundled not-found page) instead of
200 OK. If you relied on the enabled fallback returning 200, set code: 200
explicitly. The file-based fallback (ServeFile) is unaffected.
remote_ip rebuilt on axum-client-ip; trusted_proxies removed (security)This is a silent, security-relevant change. An old config's trusted_proxies:
key is now an unknown field and is ignored without error, so review your
remote_ip config before upgrading — it will not fail to load.
Previously the middleware walked X-Forwarded-For right-to-left, skipping any
address in a trusted_proxies CIDR list (or a built-in RFC-1918 + loopback list).
It now trusts exactly one configured source (source: ClientIpSource, default
RightmostXForwardedFor) and does no CIDR filtering.
set_real_ip_from / real_ip_recursive), or
point source at a provider header (CfConnectingIp, CloudFrontViewerAddress,
XRealIp, ConnectInfo, …).The RemoteIP extractor and its Display output are unchanged.
algorithm() restricted to the HMAC familyJWT::algorithm() now takes loco_rs::auth::jwt::JWTAlgorithm
(HS256 / HS384 / HS512) instead of jsonwebtoken::Algorithm. Asymmetric
algorithms — which could never work with Loco's shared base64 secret and silently
produced broken tokens — are no longer representable. If you passed a
jsonwebtoken::Algorithm, switch to the matching JWTAlgorithm variant.
TeraView::build_with_post_processIn after_routes, replace TeraView::build()?.post_process(...) with the
combined constructor:
// before
engines::TeraView::build()?.post_process(move |tera| {
tera.register_function("t", FluentLoader::new(arc.clone()));
Ok(())
})?
// after
engines::TeraView::build_with_post_process(move |tera| {
tera.register_function("t", FluentLoader::new(arc.clone()));
Ok(())
})?
Template::new(dir) now returns ResultEmail templates render through a full Tera instance (so they support inheritance
and shared templates). Standard usage via Mailer::mail_template is unchanged; if
you called Template::new(dir) directly, add ?:
let tpl = Template::new(dir)?;
Vars::cli_arg returns Result<&str>Vars::cli_arg now returns Result<&str> (was Result<&String>). Callers that
relied on &String (e.g. .clone() into a String) should use .to_owned().
1.0 bumps several dependency majors. These are transitive for most apps — you
only need to act if you use one of these crates directly through Loco's
public API: thiserror 1→2, tower 0.4→0.5, heck→0.5, byte-unit 4→5,
ipnetwork 0.20→0.21, strum→0.27, redis 0.31→1, bb8-redis→0.26,
opendal 0.54→0.57. serde_yaml (archived) was replaced by the maintained
serde_yaml_ng fork.
auth_jwt → auth.bg_redis → worker_redis; bg_pg/bg_sqlt → worker. default now includes
worker (Postgres+SQLite queues); add worker_redis for a Redis queue.integration_test removed (was dead).loco new now offers Redis/Postgres/SQLite queue backends and (serverside)
embedded assets.AppContext instead of Config in init_logger in the Hooks traitPR: #1418
If you are supplying an implementation of init_logger in your impl of the Hooks trait in order to set up your own logging, you will need to make the following change:
- fn init_logger(config: &config::Config, env: &Environment) -> Result<bool> {
+ fn init_logger(ctx: &AppContext) -> Result<bool> {
Any code in your init_logger implementation that makes use of the config can access it through ctx.config. In addition, you will also be able to access anything else in the AppContext, such as the new shared_store. The env parameter is also removed, as that is accessible from the AppContext as ctx.environment.
PR: #1359
Swap from using the loco custom email validator, to the builtin email validator from validator.
- #[validate(custom (function = "validation::is_valid_email"))]
+ #[validate(email(message = "invalid email"))]
pub email: String,
Two major changes have been made to the background job system:
The Redis background job system has been completely refactored, replacing the Sidekiq-compatible implementation with a new custom implementation. This provides greater flexibility and improved performance, but means:
A new tag-based job filtering system has been added to all background worker providers:
To upgrade to the new job system:
Process existing jobs:
Clean up old data:
FLUSHDB command)Update Loco:
PR: #1385
The cache API has been refactored to support storing and retrieving any serializable type, not just strings. This is a breaking change that requires updates to your code:
Serialize and Deserialize from serdeBefore:
// Get a string value from cache
let value = cache.get("key").await?;
// Insert or get with callback
let value = app_ctx.cache.get_or_insert("key", async {
Ok("value".to_string())
}).await.unwrap();
// Insert or get with expiry
let value = app_ctx.cache.get_or_insert_with_expiry("key", Duration::from_secs(300), async {
Ok("value".to_string())
}).await.unwrap();
After:
// Get a string value from cache - specify the type
let value = cache.get::<String>("key").await?;
// Direct insert with any serializable type
cache.insert("key", &"value".to_string()).await?;
// Insert or get with callback - specify return type
let value = app_ctx.cache.get_or_insert::<String, _>("key", async {
Ok("value".to_string())
}).await.unwrap();
// Store complex types
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
let user = app_ctx.cache.get_or_insert_with_expiry::<User, _>(
"user:1",
Duration::from_secs(300),
async {
Ok(User { name: "Alice".to_string(), age: 30 })
}
).await.unwrap();
For your custom types to work with the cache, ensure they implement Serialize and Deserialize:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyType {
// fields...
}
Authentication error handling has been improved to better distinguish between actual authorization failures and system errors:
tracing::errorIf you have code that relies on database errors during authentication returning 401 status codes, you'll need to update your error handling. Any code expecting a 401 for database connectivity issues should now handle 500 responses as well.
Client applications should be prepared to handle both 401 and 500 status codes during authentication failures, with 401 indicating authorization problems and 500 indicating system errors.
We had some changes in Tera template. go to src/initializers/view_engine.rs and replace the after_routes function with:
async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
let tera_engine = if std::path::Path::new(I18N_DIR).exists() {
let arc = std::sync::Arc::new(
ArcLoader::builder(&I18N_DIR, unic_langid::langid!("en-US"))
.shared_resources(Some(&[I18N_SHARED.into()]))
.customize(|bundle| bundle.set_use_isolating(false))
.build()
.map_err(|e| Error::string(&e.to_string()))?,
);
info!("locales loaded");
engines::TeraView::build()?.post_process(move |tera| {
tera.register_function("t", FluentLoader::new(arc.clone()));
Ok(())
})?
} else {
engines::TeraView::build()?
};
Ok(router.layer(Extension(ViewEngine::from(tera_engine))))
}
PR: #1199
Update the validator crate version in your Cargo.toml:
From
validator = { version = "0.19" }
To
validator = { version = "0.20" }
PR: #1159
Flattened (De)Serialization of Custom User Claims:
The claims field in UserClaims has changed from Option<Value> to Map<String, Value>.
Mandatory Map Value in generate_token function:
When calling generate_token, the Map<String, Value> argument is now required. If you are not using custom claims, pass an empty map (serde_json::Map::new()).
Updated generate_token Signature:
The generate_token function now takes expiration as a value instead of a reference.
PR: #1197
The pagination response now includes the total_items field, providing the total number of items available.
{"results":[],"pagination":{"page":0,"page_size":0,"total_pages":0,"total_items":0}}
PR: #1268
Migrations using create_table now require ("id", ColType::PkAuto), new migrations will have this field automatically added.
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table(m, "movies",
&[
+ ("id", ColType::PkAuto),
("title", ColType::StringNull),
],
&[
("user", ""),
]
).await
}
PR: #1130 The upgrade to Axum 0.8 introduces a breaking change. For more details, refer to the announcement.
Cargo.toml, update the Axum version from 0.7.5 to 0.8.1.axum::async_trait; with use async_trait::async_trait;. For more information, see here./:single and /*many to /{single} and /{*many}.boot Function HookPR: #1143
The boot hook function now accepts an additional Config parameter. The function signature has changed from:
From
async fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment).await
}
To:
async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment, config).await
}
Make sure to import the Config type as needed.
PR: #993
Update the validator crate version in your Cargo.toml:
From
validator = { version = "0.18" }
To
validator = { version = "0.19" }
PR: #1158
The truncate and seed functions now receive AppContext instead of DatabaseConnection as their argument.
From
async fn truncate(db: &DatabaseConnection) -> Result<()> {}
async fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {}
To
async fn truncate(ctx: &AppContext) -> Result<()> {}
async fn seed(_ctx: &AppContext, base: &Path) -> Result<()> {}
Impact on Testing:
Testing code involving the seed function must also be updated accordingly.
from:
async fn load_page() {
request::<App, _, _>(|request, ctx| async move {
seed::<App>(&ctx.db).await.unwrap();
...
})
.await;
}
to
async fn load_page() {
request::<App, _, _>(|request, ctx| async move {
seed::<App>(&ctx).await.unwrap();
...
})
.await;
}