Session data
The Session
type is the main interface to work with sessions in Pavex.
You can think of the session state as a collection of key-value pairs. The keys are strings, the values are
arbitrary—e.g. strings, numbers, or even complex data structures1.
Let's go through a few examples to get familiar with the basic operations you can perform.
Storing data
Use insert
to store an entry in the server-side state of your session:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_> /* (1)! */) -> Result<Response, Error> {
session.insert("user.id", "my-user-identifier").await?;
// [...]
- Pavex knows how to inject a
&mut Session
or a&Session
as an input parameter. It's using the session kit you installed which specifies how to construct aSession
instance.
In the example above, insert
will create a new user.id
entry in the session state.
If there is already a user.id
entry, it'll overwrite it.
Complex objects
You're not limited to storing simple values. You can store complex types like structs or enums as well, if they are serializable:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
#[derive(serde::Serialize, serde::Deserialize)] // (1)!
struct AuthInfo {
user_id: String,
email: String,
}
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
let info = AuthInfo {
user_id: "my-user-identifier".into(),
email: "[email protected]".into(),
};
session.insert("user", info).await?; // (2)!
// [...]
}
- We derive
serde
'sSerialize
andDeserialize
traits to ensure the type can be serialized and deserialized. insert
will return an error if the serialization step fails.
Retrieving data
Use get
to retrieve an entry from the server-side state of your session:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &Session<'_> /* (1)! */) -> Result<Response, Error> {
let user_id: Option<String> /* (2)! */ = session.get("user.id").await?; // (3)!
// [...]
}
get
doesn't modify the session state, it only reads from it. It is therefore enough to ask for a shared reference to the session, rather than a mutable one. If you need to both read and write to the session state, ask for a mutable reference.get
returns anOption
because the key might not exist in the session state. We specify the type of the value we expect to get back so that [get
] knows what to deserialize the value as, if it exists.get
may fail if the value is not of the expected type, thus failing the deserialization step.
Complex objects
The process is exactly the same for more complex types. You just need to specify the type you expect to get back:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
#[derive(serde::Serialize, serde::Deserialize)] // (1)!
struct AuthInfo {
user_id: String,
email: String,
}
pub async fn handler(session: &Session<'_>) -> Result<Response, Error> {
let auth_info: Option<AuthInfo> /* (2)! */ = session.get("user").await?;
// [...]
}
- The extracted type must be deserializeable. That's why we derive
serde
'sDeserialize
trait. - The type annotation tells
get
to deserialize the value as anAuthInfo
instance, rather than a string, as in the previous example.
Removing data
Use remove
to delete an entry from the server-side state of your session:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
let user_id: Option<String> /* (1)! */ = session.remove("user.id").await?;
// [...]
}
- You must add a type annotation to tell
remove
what type of value you expect to get back.
Remove returns the entry that was removed, if it existed, or None
otherwise.
If you don't plan to use the removed value, you can invoke remove_raw
instead:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
session.remove_raw("user.id").await?;
// [...]
}
It returns the raw entry, without trying to deserialize it. It spares you from having to specify the type.
Regenerating the session ID
Your application may be required to regenerate the session ID
to prevent session fixation attacks.
You can do this by calling cycle_id
:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
session.cycle_id();
// [...]
}
cycle_id
doesn't change the session state in any way.
Session invalidation
If you want to destroy the current session, call invalidate
:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
session.invalidate();
// [...]
}
invalidate
will:
- remove the server-side state from the storage backend
- delete the session cookie on the client-side
It effectively ends the current session.
All operations on the current session after invoking invalidate
will be ignored.
Deletion without invalidation
There may be situations where you want to keep the session alive, but remove all data
from the server-side state. Use clear
:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
session.clear().await?;
// [...]
}
clear
removes all entries from the server-side state, leaving an empty record
in the storage backend.
If you want to delete the server-side state entry completely, use delete
:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
session.delete();
// [...]
}
delete
will remove the server-side state entry from the storage backend, but it won't
delete the session cookie on the client-side.
Client-side state
As we discussed in the introduction, there are two types of session data:
the client-side state and the server-side state.
All the examples above manipulate the server-side state, but there may be cases where you want to
store data in the client-side state to minimize the number of round-trips to the session storage
backend.
You can use client
and client_mut
to perform the same operations on the client-side state:
use anyhow::Error;
use pavex::response::Response;
use pavex_session::Session;
pub async fn handler(session: &mut Session<'_>) -> Result<Response, Error> {
let key = "user.id";
let value = "my-user-identifier";
// Insertion
session.client_mut().insert(key, value)?;
// Retrieval
let stored: Option<String> = session.client().get(key)?;
assert_eq!(stored.as_deref(), Some(value));
// Removal
session.client_mut().remove_raw(key);
assert_eq!(session.client().get_raw(key), None);
// [...]
}
Keep in mind that the client-side state is stored inside the session cookie.
It's not suitable for storing large amounts of data and it is inherently more exposed than its
server-side counterpart. Use it only for small, non-sensitive data.
-
Internally, each value is stored as a JSON object. This means that you can store any type that can be serialized to (and deserialized from) JSON. In Rust terms, you can reason about the session state as if it were a
HashMap<String, serde_json::Value>
. ↩