|
1 | 1 | # Sessions
|
2 | 2 |
|
3 |
| -The `Session` object provides information about the current context in a method call in Serverpod. It provides access to the database, caching, authentication, data storage, and messaging within the server. It will also contain information about the HTTP request object. |
| 3 | +A Session in Serverpod is a request-scoped context object that exists for the duration of a single client request or connection. It provides access to server resources and maintains state during request processing. |
4 | 4 |
|
5 |
| -If you need additional information about a call, you may need to cast the Session to one of its subclasses, e.g., `MethodCallSession` or `StreamingSession`. The `MethodCallSession` object provides additional properties, such as the name of the endpoint and method and the underlying `HttpRequest` object. |
| 5 | +Sessions are the gateway to Serverpod's functionality - every interaction with the database, cache, file storage, or messaging system happens through a session. The framework automatically creates the appropriate session type when a client makes a request, manages its lifecycle, and ensures proper cleanup when the request completes. For special cases like background tasks or system operations, you can also create and manage sessions manually. |
6 | 6 |
|
7 |
| -:::tip |
| 7 | +:::note |
| 8 | + |
| 9 | +A Serverpod Session should not be confused with the concept of "web sessions" or "user sessions" which persist over multiple API calls. See the [Authentication documentation](./11-authentication/01-setup.md) for managing persistent authentication. |
| 10 | + |
| 11 | +::: |
| 12 | + |
| 13 | +## Quick reference |
| 14 | + |
| 15 | +### Essential properties |
| 16 | + |
| 17 | +- **`db`** - Database access. [See database docs](./06-database/01-connection.md) |
| 18 | +- **`caches`** - Local and distributed caching. [See caching docs](./08-caching.md) |
| 19 | +- **`storage`** - File storage operations. [See file uploads](./12-file-uploads.md) |
| 20 | +- **`messages`** - Server events for real-time communication within and across servers. [See server events docs](./16-server-events.md) |
| 21 | +- **`passwords`** - Credentials from config and environment. [See configuration](./07-configuration.md) |
| 22 | +- **`authenticated`** - Current user authentication info. [See authentication docs](./11-authentication/02-basics.md) |
| 23 | + |
| 24 | +### Key methods |
| 25 | + |
| 26 | +- **`log(message, level)`** - Add log entry |
| 27 | +- **`addWillCloseListener(callback)`** - Register cleanup callback |
| 28 | + |
| 29 | +## Session types |
| 30 | + |
| 31 | +Serverpod creates different session types based on the context: |
| 32 | + |
| 33 | +| Type | Created For | Lifetime | Common Use Cases | |
| 34 | +| ----------------------- | ---------------------------- | ------------------- | ---------------------------- | |
| 35 | +| **MethodCallSession** | `Future<T>` endpoint methods | Single request | API calls, CRUD operations | |
| 36 | +| **WebCallSession** | Web server routes | Single request | Web pages, form submissions | |
| 37 | +| **MethodStreamSession** | `Stream<T>` endpoint methods | Stream duration | Real-time updates, chat | |
| 38 | +| **StreamingSession** | WebSocket connections | Connection duration | Live dashboards, multiplayer | |
| 39 | +| **FutureCallSession** | Scheduled tasks | Task execution | Email sending, batch jobs | |
| 40 | +| **InternalSession** | Manual creation | Until closed | Background work, migrations | |
8 | 41 |
|
9 |
| -You can use the Session object to access the IP address of the client calling a method. Serverpod includes an extension on `HttpRequest` that allows you to access the IP address even if your server is running behind a load balancer. |
| 42 | +### Example: Automatic session (MethodCallSession) |
10 | 43 |
|
11 | 44 | ```dart
|
12 |
| -session as MethodCallSession; |
13 |
| -var ipAddress = session.httpRequest.remoteIpAddress; |
| 45 | +// lib/src/endpoints/example_endpoint.dart |
| 46 | +class ExampleEndpoint extends Endpoint { |
| 47 | + Future<String> hello(Session session, String name) async { |
| 48 | + // MethodCallSession is created automatically |
| 49 | + return 'Hello $name'; |
| 50 | + // Session closes automatically when method returns |
| 51 | + } |
| 52 | +} |
14 | 53 | ```
|
15 | 54 |
|
16 |
| -::: |
| 55 | +### Example: Manual session (InternalSession) |
| 56 | + |
| 57 | +InternalSession is the only session type that requires manual management: |
| 58 | + |
| 59 | +```dart |
| 60 | +Future<void> performMaintenance() async { |
| 61 | + var session = await Serverpod.instance.createSession(); |
| 62 | + try { |
| 63 | + // Perform operations |
| 64 | + await cleanupOldRecords(session); |
| 65 | + await updateStatistics(session); |
| 66 | + } finally { |
| 67 | + await session.close(); // Must close manually! |
| 68 | + } |
| 69 | +} |
| 70 | +``` |
17 | 71 |
|
18 |
| -## Creating sessions |
| 72 | +**Important**: Always use try-finally with InternalSession to prevent memory leaks. |
19 | 73 |
|
20 |
| -In most cases, Serverpod manages the life cycle of the Session objects for you. A session is created for a call or a streaming connection and is disposed of when the call has been completed. In rare cases, you may want to create a session manually. For instance, if you are making a database call outside the scope of a method or a future call. In these cases, you can create a new session with the `createSession` method of the `Serverpod` singleton. You can access the singleton by the static `Serverpod.instance` field. If you create a new session, you are also responsible for closing it using the `session.close()` method. |
| 74 | +## Session lifecycle |
21 | 75 |
|
22 |
| -:::note |
| 76 | +```mermaid |
| 77 | +flowchart TB |
| 78 | + Request([Request/Trigger]) --> Create[Session Created] |
| 79 | + Create --> Init[Initialize] |
| 80 | + Init --> Active[Execute Method] |
| 81 | + Active --> Close[Close Session] |
| 82 | + Close --> End([Request Complete]) |
| 83 | +``` |
| 84 | + |
| 85 | +Sessions follow a predictable lifecycle from creation to cleanup. When a client makes a request, Serverpod automatically creates the appropriate session type (see table above), initializes it with a unique ID, and sets up access to resources like the database, cache, and file storage. |
| 86 | + |
| 87 | +During the active phase, your operation executes with full access to Serverpod resources through the session. You can query the database, write logs, send messages, and access storage - all operations are tracked and tied to this specific session. When the operation completes, most sessions close automatically, writing any accumulated logs to the database and releasing all resources. |
| 88 | + |
| 89 | +### Internal Sessions |
| 90 | + |
| 91 | +The only exception is `InternalSession`, which you create manually for background tasks. Manual sessions require explicit closure with `session.close()`. Forgetting to close these sessions causes memory leaks as logs accumulate indefinitely. Always use try-finally blocks to ensure proper cleanup. After any session closes, attempting to use it throws a `StateError`. |
23 | 92 |
|
24 |
| -It's not recommended to keep a session open indefinitely as it can lead to memory leaks, as the session stores logs until it is closed. It's inexpensive to create a new session, so keep them short. |
| 93 | +### Session cleanup callbacks |
| 94 | + |
| 95 | +You can register callbacks that execute just before a session closes using `addWillCloseListener`. This is useful for cleanup operations, releasing resources, or performing final operations: |
| 96 | + |
| 97 | +```dart |
| 98 | +Future<void> processData(Session session) async { |
| 99 | + var tempFile = File('/tmp/processing_data.tmp'); |
| 100 | +
|
| 101 | + // Register cleanup callback |
| 102 | + session.addWillCloseListener((session) async { |
| 103 | + if (await tempFile.exists()) { |
| 104 | + await tempFile.delete(); |
| 105 | + session.log('Cleaned up temporary file'); |
| 106 | + } |
| 107 | + }); |
| 108 | +
|
| 109 | + // Process data using temp file |
| 110 | + await tempFile.writeAsString('processing...'); |
| 111 | + // Session closes automatically, cleanup callback runs |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +Cleanup callbacks run in the order they were registered and are called for all session types, including manual sessions when you call `session.close()`. |
| 116 | + |
| 117 | +## Logging |
| 118 | + |
| 119 | +Serverpod batches log entries for performance. During normal operations, logs accumulate in memory and are written to the database in a single batch when the session closes. This includes all your `session.log()` calls, database query timings, and session metadata. The exception is streaming sessions (`MethodStreamSession` and `StreamingSession`), which write logs continuously by default to avoid memory buildup during long connections. |
| 120 | + |
| 121 | +:::warning |
| 122 | + |
| 123 | +If you forget to close a manual session (`InternalSession`), logs remain in memory indefinitely and are never persisted - this is a common cause of both memory leaks and missing debug information. |
25 | 124 |
|
26 | 125 | :::
|
| 126 | + |
| 127 | +## Common pitfalls and solutions |
| 128 | + |
| 129 | +### Pitfall 1: Using session after method returns |
| 130 | + |
| 131 | +**Problem:** Using a session after it's closed throws a `StateError` |
| 132 | + |
| 133 | +```dart |
| 134 | +Future<void> processUser(Session session, int userId) async { |
| 135 | + var user = await User.db.findById(session, userId); |
| 136 | +
|
| 137 | + // Schedule async work |
| 138 | + Timer(Duration(seconds: 5), () async { |
| 139 | + // ❌ Session is already closed! |
| 140 | + // This will throw: StateError: Session is closed |
| 141 | + await user.updateLastSeen(session); |
| 142 | + }); |
| 143 | +
|
| 144 | + return; // Session closes here |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +**Solution 1 - Use FutureCalls:** |
| 149 | + |
| 150 | +```dart |
| 151 | +Future<void> processUser(Session session, int userId) async { |
| 152 | + var user = await User.db.findById(session, userId); |
| 153 | +
|
| 154 | + // Schedule through Serverpod |
| 155 | + await session.serverpod.futureCallWithDelay( |
| 156 | + 'updateLastSeen', |
| 157 | + UserIdData(userId: userId), |
| 158 | + Duration(seconds: 5), |
| 159 | + ); |
| 160 | +
|
| 161 | + return; |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +**Solution 2 - Create manual session:** |
| 166 | + |
| 167 | +```dart |
| 168 | +Future<void> processUser(Session session, int userId) async { |
| 169 | + var user = await User.db.findById(session, userId); |
| 170 | +
|
| 171 | + Timer(Duration(seconds: 5), () async { |
| 172 | + // Create new session for async work |
| 173 | + var newSession = await Serverpod.instance.createSession(); |
| 174 | + try { |
| 175 | + await user.updateLastSeen(newSession); |
| 176 | + } finally { |
| 177 | + await newSession.close(); |
| 178 | + } |
| 179 | + }); |
| 180 | +
|
| 181 | + return; |
| 182 | +} |
| 183 | +``` |
| 184 | + |
| 185 | +### Pitfall 2: Forgetting to close manual sessions |
| 186 | + |
| 187 | +**Problem:** |
| 188 | + |
| 189 | +```dart |
| 190 | +// ❌ Memory leak! |
| 191 | +var session = await Serverpod.instance.createSession(); |
| 192 | +var users = await User.db.find(session); |
| 193 | +// Forgot to close - session leaks memory |
| 194 | +``` |
| 195 | + |
| 196 | +**Solution - Always use try-finally:** |
| 197 | + |
| 198 | +```dart |
| 199 | +var session = await Serverpod.instance.createSession(); |
| 200 | +try { |
| 201 | + var users = await User.db.find(session); |
| 202 | + // Process users |
| 203 | +} finally { |
| 204 | + await session.close(); // Always runs |
| 205 | +} |
| 206 | +``` |
| 207 | + |
| 208 | +## Best practices |
| 209 | + |
| 210 | +### 1. Let Serverpod manage sessions when possible |
| 211 | + |
| 212 | +Prefer using the session provided to your endpoint rather than creating new ones: |
| 213 | + |
| 214 | +```dart |
| 215 | +// ✅ Good - Use provided session |
| 216 | +Future<List<User>> getActiveUsers(Session session) async { |
| 217 | + return await User.db.find( |
| 218 | + session, |
| 219 | + where: (t) => t.isActive.equals(true), |
| 220 | + ); |
| 221 | +} |
| 222 | +
|
| 223 | +// ❌ Avoid - Creating unnecessary session |
| 224 | +Future<List<User>> getActiveUsers(Session session) async { |
| 225 | + var newSession = await Serverpod.instance.createSession(); |
| 226 | + try { |
| 227 | + return await User.db.find(newSession, ...); |
| 228 | + } finally { |
| 229 | + await newSession.close(); |
| 230 | + } |
| 231 | +} |
| 232 | +``` |
| 233 | + |
| 234 | +### 2. Use FutureCalls for delayed operations |
| 235 | + |
| 236 | +Instead of managing sessions for async work, use Serverpod's future call system: |
| 237 | + |
| 238 | +```dart |
| 239 | +// ✅ Good - Let Serverpod manage the session |
| 240 | +await serverpod.futureCallWithDelay( |
| 241 | + 'processPayment', |
| 242 | + PaymentData(orderId: order.id), |
| 243 | + Duration(hours: 1), |
| 244 | +); |
| 245 | +
|
| 246 | +// ❌ Complex - Manual session management |
| 247 | +Future.delayed(Duration(hours: 1), () async { |
| 248 | + var session = await Serverpod.instance.createSession(); |
| 249 | + try { |
| 250 | + await processPayment(session, order.id); |
| 251 | + } finally { |
| 252 | + await session.close(); |
| 253 | + } |
| 254 | +}); |
| 255 | +``` |
| 256 | + |
| 257 | +### 3. Handle errors properly |
| 258 | + |
| 259 | +Always handle exceptions to prevent unclosed sessions: |
| 260 | + |
| 261 | +```dart |
| 262 | +// ✅ Good - Errors won't prevent session cleanup |
| 263 | +Future<void> safeOperation() async { |
| 264 | + var session = await Serverpod.instance.createSession(); |
| 265 | + try { |
| 266 | + await riskyOperation(session); |
| 267 | + } catch (e) { |
| 268 | + session.log('Operation failed: $e', level: LogLevel.error); |
| 269 | + // Handle error appropriately |
| 270 | + } finally { |
| 271 | + await session.close(); |
| 272 | + } |
| 273 | +} |
| 274 | +``` |
| 275 | + |
| 276 | +## Testing |
| 277 | + |
| 278 | +When testing endpoints, the `TestSessionBuilder` can be used to simulate and configure session properties for controlled test scenarios: |
| 279 | + |
| 280 | +```dart |
| 281 | +withServerpod('test group', (sessionBuilder, endpoints) { |
| 282 | + test('endpoint test', () async { |
| 283 | + var result = await endpoints.users.getUser(sessionBuilder, 123); |
| 284 | + expect(result.name, 'John'); |
| 285 | + }); |
| 286 | +
|
| 287 | + test('authenticated endpoint test', () async { |
| 288 | + const int userId = 1234; |
| 289 | +
|
| 290 | + var authenticatedSessionBuilder = sessionBuilder.copyWith( |
| 291 | + authentication: AuthenticationOverride.authenticationInfo(userId, {Scope('user')}), |
| 292 | + ); |
| 293 | +
|
| 294 | + var result = await endpoints.users.updateProfile( |
| 295 | + authenticatedSessionBuilder, |
| 296 | + ProfileData(name: 'Jane') |
| 297 | + ); |
| 298 | + expect(result.success, isTrue); |
| 299 | + }); |
| 300 | +}); |
| 301 | +``` |
| 302 | + |
| 303 | +For detailed testing strategies, see the [testing documentation](./19-testing/01-get-started.md). |
0 commit comments