pub struct SubmitTokenConfig {
pub enabled: bool,
pub field_name: String,
pub ttl_secs: u64,
pub in_flight_ttl_secs: u64,
pub backend: Option<IdempotencyBackend>,
pub exempt_paths: Vec<String>,
}Expand description
One-time submit-token protection settings.
When enabled (the default), a per-render random token is exposed via the
SubmitToken extractor and embedded as a
hidden _submit_token field in scaffolded create/update forms. On the
mutating POST the server consumes the token exactly once: a double-click,
Back→resubmit, or browser retry carrying an already-consumed token replays
the first response instead of re-running the handler, so no duplicate row is
created — with no client-side JavaScript.
Unlike IdempotencyConfig, the guard is
driven by a form field, not the Idempotency-Key header, so it protects
bare browser form submits.
§Defaults
| Field | Default |
|---|---|
enabled | true |
field_name | "_submit_token" |
ttl_secs | 600 (10 min) |
in_flight_ttl_secs | 86_400 (24 h) |
backend | inherits [idempotency].backend (in-memory in dev, Redis in prod) |
exempt_paths | [] |
§Examples
[security.submit_token]
enabled = true
ttl_secs = 900
backend = "redis" # override; reuses the [idempotency.redis] connection settingsFields§
§enabled: boolEnable one-time submit-token protection. Default: true.
field_name: StringHidden form field name carrying the token. Default: "_submit_token".
ttl_secs: u64Time-to-live in seconds for a consumed token’s stored response.
Default: 600 (10 minutes).
in_flight_ttl_secs: u64Maximum stale lifetime in seconds for an in-flight submission lock.
While a mutating request is running, its token is locked so a concurrent
retry carrying the same token is excluded until the first request records
its consumed response. The lock is released as soon as that record is
stored, so this value is only the backend safety expiry for crashes or
lost unlocks — it must be comfortably longer than any supported mutating
request duration. Deliberately independent of ttl_secs (the replay
window): lowering ttl_secs must never shorten how long an active
submission is excluded from re-entry, which would let a slow request’s
retry acquire a fresh lock and double-execute. Default: 86_400
(24 hours), matching [idempotency].in_flight_ttl_secs.
backend: Option<IdempotencyBackend>Storage backend for consumed submit tokens.
When unset (the default, None), the submit-token store inherits the
configured idempotency backend ([idempotency].backend): a
Redis-configured app automatically shares one consumed-token store across
replicas, while a dev app on the default in-memory idempotency backend
keeps an in-memory token store. This matches issue #1360: the token store
is backed by the existing idempotency/session store backend (in-memory in
dev, Redis in prod), so a double-click load-balanced to a different
replica cannot re-run the mutation in production.
Set explicitly to override the inherited backend for submit tokens only.
When it resolves to "redis", the store reuses the [idempotency.redis]
connection settings so a multi-replica deployment shares one token store.
Use Self::resolved_backend to obtain the effective backend.
exempt_paths: Vec<String>Request path prefixes that are exempt from submit-token guarding.
Default: [].
Implementations§
Source§impl SubmitTokenConfig
impl SubmitTokenConfig
Sourcepub fn resolved_backend(
&self,
idempotency_backend: IdempotencyBackend,
) -> IdempotencyBackend
pub fn resolved_backend( &self, idempotency_backend: IdempotencyBackend, ) -> IdempotencyBackend
Resolve the effective consumed-token storage backend.
Returns the explicit backend override when one is configured;
otherwise inherits idempotency_backend (the app’s
[idempotency].backend) so submit tokens share the idempotency store by
default. This is the single source of truth for backend selection so the
idempotency layer and the submit-token layer cannot drift apart.
Trait Implementations§
Source§impl Clone for SubmitTokenConfig
impl Clone for SubmitTokenConfig
Source§fn clone(&self) -> SubmitTokenConfig
fn clone(&self) -> SubmitTokenConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for SubmitTokenConfig
impl Debug for SubmitTokenConfig
Source§impl Default for SubmitTokenConfig
impl Default for SubmitTokenConfig
Source§impl<'de> Deserialize<'de> for SubmitTokenConfig
impl<'de> Deserialize<'de> for SubmitTokenConfig
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Auto Trait Implementations§
impl Freeze for SubmitTokenConfig
impl RefUnwindSafe for SubmitTokenConfig
impl Send for SubmitTokenConfig
impl Sync for SubmitTokenConfig
impl Unpin for SubmitTokenConfig
impl UnsafeUnpin for SubmitTokenConfig
impl UnwindSafe for SubmitTokenConfig
Blanket Implementations§
Source§impl<T> AggregateExpressionMethods for T
impl<T> AggregateExpressionMethods for T
Source§fn aggregate_distinct(self) -> Self::Outputwhere
Self: DistinctDsl,
fn aggregate_distinct(self) -> Self::Outputwhere
Self: DistinctDsl,
DISTINCT modifier for aggregate functions Read moreSource§fn aggregate_all(self) -> Self::Outputwhere
Self: AllDsl,
fn aggregate_all(self) -> Self::Outputwhere
Self: AllDsl,
ALL modifier for aggregate functions Read moreSource§fn aggregate_filter<P>(self, f: P) -> Self::Output
fn aggregate_filter<P>(self, f: P) -> Self::Output
Source§fn aggregate_order<O>(self, o: O) -> Self::Outputwhere
Self: OrderAggregateDsl<O>,
fn aggregate_order<O>(self, o: O) -> Self::Outputwhere
Self: OrderAggregateDsl<O>,
Source§impl<T> AutumnDependents for Twhere
T: ?Sized,
impl<T> AutumnDependents for Twhere
T: ?Sized,
Source§fn dependents() -> &'static [RuntimeDependentSpec]
fn dependents() -> &'static [RuntimeDependentSpec]
#[model] overrides via an inherent shadow when dependents exist.Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoSql for T
impl<T> IntoSql for T
Source§fn into_sql<T>(self) -> Self::Expression
fn into_sql<T>(self) -> Self::Expression
self to an expression for Diesel’s query builder. Read moreSource§fn as_sql<'a, T>(&'a self) -> <&'a Self as AsExpression<T>>::Expression
fn as_sql<'a, T>(&'a self) -> <&'a Self as AsExpression<T>>::Expression
&self to an expression for Diesel’s query builder. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<T> RepositoryHooksClone for Twhere
T: Clone,
impl<T> RepositoryHooksClone for Twhere
T: Clone,
Source§fn autumn_clone(&self) -> T
fn autumn_clone(&self) -> T
Source§impl<T> RepositoryHooksDefault for Twhere
T: Default,
impl<T> RepositoryHooksDefault for Twhere
T: Default,
Source§fn autumn_default() -> T
fn autumn_default() -> T
Source§impl<T, Conn> RunQueryDsl<Conn> for T
impl<T, Conn> RunQueryDsl<Conn> for T
Source§fn execute<'conn, 'query>(
self,
conn: &'conn mut Conn,
) -> <Conn as AsyncConnectionCore>::ExecuteFuture<'conn, 'query>
fn execute<'conn, 'query>( self, conn: &'conn mut Conn, ) -> <Conn as AsyncConnectionCore>::ExecuteFuture<'conn, 'query>
Source§fn load<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
fn load<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
Source§fn load_stream<'conn, 'query, U>(
self,
conn: &'conn mut Conn,
) -> Self::LoadFuture<'conn>where
Conn: AsyncConnectionCore,
U: 'conn,
Self: LoadQuery<'query, Conn, U> + 'query,
fn load_stream<'conn, 'query, U>(
self,
conn: &'conn mut Conn,
) -> Self::LoadFuture<'conn>where
Conn: AsyncConnectionCore,
U: 'conn,
Self: LoadQuery<'query, Conn, U> + 'query,
Stream] with the returned rows. Read moreSource§fn get_result<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, LoadNext<Pin<Box<Self::Stream<'conn>>>>>
fn get_result<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, LoadNext<Pin<Box<Self::Stream<'conn>>>>>
Source§fn get_results<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
fn get_results<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
Vec with the affected rows. Read moreSource§impl<T> Scoped for T
impl<T> Scoped for T
Source§fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self>
fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self>
ScopeQuery for this type. Resolves the
registered scope at .load() time, not here.