From d92ca505c3e4cb4ec7f8ffb9950549a5ac88a639 Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Fri, 17 Jul 2026 09:23:06 +0100 Subject: [PATCH] docs: Deepen the Endpoints and APIs section with verified 4.0 behavior --- docs/04-get-started/03-how-it-works.md | 4 +- .../01-working-with-endpoints.md | 135 ++++---- .../02-endpoints-and-apis/02-sessions.md | 290 +++++------------- ...md => 03-error-handling-and-exceptions.md} | 28 +- .../02-endpoints-and-apis/04-streaming.md | 196 ++++++++++++ .../05-endpoint-middleware.md | 95 ------ .../02-endpoints-and-apis/05-server-events.md | 98 ++++++ ...{09-file-uploads.md => 06-file-uploads.md} | 52 ++-- ...eritance.md => 07-endpoint-inheritance.md} | 19 +- .../02-endpoints-and-apis/07-streaming.md | 166 ---------- .../08-endpoint-middleware.md | 141 +++++++++ .../02-endpoints-and-apis/08-server-events.md | 112 ------- ...tp-calls.md => 09-configure-http-calls.md} | 10 +- .../05-token-managers/02-jwt-token-manager.md | 2 +- .../cli/commands/generate/_generate.md | 2 +- 15 files changed, 631 insertions(+), 719 deletions(-) rename docs/06-concepts/02-endpoints-and-apis/{06-error-handling-and-exceptions.md => 03-error-handling-and-exceptions.md} (73%) create mode 100644 docs/06-concepts/02-endpoints-and-apis/04-streaming.md delete mode 100644 docs/06-concepts/02-endpoints-and-apis/05-endpoint-middleware.md create mode 100644 docs/06-concepts/02-endpoints-and-apis/05-server-events.md rename docs/06-concepts/02-endpoints-and-apis/{09-file-uploads.md => 06-file-uploads.md} (72%) rename docs/06-concepts/02-endpoints-and-apis/{03-endpoint-inheritance.md => 07-endpoint-inheritance.md} (90%) delete mode 100644 docs/06-concepts/02-endpoints-and-apis/07-streaming.md create mode 100644 docs/06-concepts/02-endpoints-and-apis/08-endpoint-middleware.md delete mode 100644 docs/06-concepts/02-endpoints-and-apis/08-server-events.md rename docs/06-concepts/02-endpoints-and-apis/{04-configure-http-calls.md => 09-configure-http-calls.md} (86%) diff --git a/docs/04-get-started/03-how-it-works.md b/docs/04-get-started/03-how-it-works.md index 39b74986..ca015601 100644 --- a/docs/04-get-started/03-how-it-works.md +++ b/docs/04-get-started/03-how-it-works.md @@ -50,7 +50,7 @@ Start your project before you begin building. With `serverpod start` already run ## Write an endpoint -In Serverpod, endpoints are the entry points your app calls to run code on the server. You define one as a class that extends `Endpoint`, with async methods that each take a [`Session`](./concepts/endpoints-and-apis/sessions) as their first argument and return a typed `Future`: +In Serverpod, endpoints are the entry points your app calls to run code on the server. You define one as a class that extends `Endpoint`, with async methods that each take a [`Session`](./concepts/endpoints-and-apis/sessions) as their first argument and return a typed `Future` (or a `Stream`, for live updates): ```dart class ExampleEndpoint extends Endpoint { @@ -70,7 +70,7 @@ On the app side, the generated client turns each endpoint method into what looks final greeting = await client.example.hello('World'); ``` -The client handles the request, the response, and the JSON in between. Most calls follow this request-and-response shape. For live updates, Serverpod also has [streaming endpoints](./concepts/endpoints-and-apis/streaming) that keep a connection open so the server and app can push data to each other. +The client handles the request, the response, and the JSON in between. Most calls follow this request-and-response shape. For live updates, Serverpod also has [streaming methods](./concepts/endpoints-and-apis/streaming) that keep a connection open so the server and app can push data to each other. ## Define your data models diff --git a/docs/06-concepts/02-endpoints-and-apis/01-working-with-endpoints.md b/docs/06-concepts/02-endpoints-and-apis/01-working-with-endpoints.md index 49c4bc87..f4397819 100644 --- a/docs/06-concepts/02-endpoints-and-apis/01-working-with-endpoints.md +++ b/docs/06-concepts/02-endpoints-and-apis/01-working-with-endpoints.md @@ -6,17 +6,17 @@ description: Endpoints expose server methods to a generated, typed client your F # Working with endpoints -Endpoints define the server methods that the client can call. With Serverpod, you add methods to your endpoint, and your client code will be generated to make the method call. +Endpoints are how your app talks to your server: you write a method on the server, and Serverpod generates a typed Dart method your app calls directly, with no REST layer, JSON parsing, or API contract to maintain. This page covers defining endpoints, calling them from your app, and pointing the app at each environment. The rest of this section builds on it, page by page: [sessions](./endpoints-and-apis/sessions), [error handling](./endpoints-and-apis/error-handling-and-exceptions), [streaming](./endpoints-and-apis/streaming), [server events](./endpoints-and-apis/server-events), and [file uploads](./endpoints-and-apis/file-uploads). See [Related](#related) for the full map. For the client code to be generated: - Place the endpoint file anywhere under the `lib` directory of your server. - Create a class that extends `Endpoint`. -- Define methods that return a typed `Future` and take a `Session` object as their first argument. +- Define methods that return a typed `Future` (or a `Stream`, see [Streaming](./endpoints-and-apis/streaming)) and take a `Session` object as their first argument. -The `Session` object holds information about the call being made and provides access to the database. +The [`Session`](./endpoints-and-apis/sessions) object holds information about the call being made and provides access to the database. -## Creating an endpoint +## Create an endpoint ```dart import 'package:serverpod/serverpod.dart'; @@ -28,99 +28,92 @@ class ExampleEndpoint extends Endpoint { } ``` -The above code will create an endpoint called `example` (the Endpoint suffix will be removed) with the single `hello` method. While [`serverpod start`](./server-fundamentals/running-your-server) is running, the client-side code is generated automatically when you save. Outside a session, run `serverpod generate` in the server directory instead. +The above code creates an endpoint called `example` (the Endpoint suffix is removed) with the single `hello` method. While [`serverpod start`](./server-fundamentals/running-your-server) is running, the client-side code is generated automatically when you save. Outside a start session, run `serverpod generate` (or `serverpod generate --watch`) in the server directory. -:::info +## Call an endpoint from your app -You can pass the `--watch` flag to `serverpod generate` to watch for changed files and regenerate without a `serverpod start` session. - -::: - -## Calling an endpoint - -On the client side, you can now call the method by calling: +Your app calls the method through the generated client: ```dart var result = await client.example.hello('World'); ``` -Initialize the generated client once and keep it somewhere that the rest of your app can reuse, such as a service locator: +The scaffolded Flutter app already creates that client in `lib/main.dart`, connected to your development server: ```dart -// Sets up a singleton client object that can be used to talk to the server from -// anywhere in our app. The client is generated from your server code. -// The client is set up to connect to a Serverpod running on a local server on -// the default port. You will need to modify this to connect to staging or -// production servers. -var client = Client('http://localhost:8080/') - ..connectivityMonitor = FlutterConnectivityMonitor(); -``` +late final Client client; -If you run the app in an Android emulator, use `10.0.2.2` instead of `localhost`, since `10.0.2.2` is the IP address of the host machine from inside the emulator. To access the server from a different device on the same network (such as a physical phone), replace `localhost` with the local IP address of your machine. You can find the local IP by running `ifconfig` (Linux/macOS) or `ipconfig` (Windows). +void main() async { + WidgetsFlutterBinding.ensureInitialized(); -Make sure to also update the `publicHost` in the development config so the server always serves the client with the correct path to assets and other resources. See [Configuration](./server-fundamentals/configuration) for how the config files work. + final serverUrl = await getServerUrl(); -```yaml -# your_project_server/config/development.yaml + client = Client(serverUrl) + ..connectivityMonitor = FlutterConnectivityMonitor(); -apiServer: - port: 8080 - publicHost: localhost # Change this line - publicPort: 8080 - publicScheme: http + runApp(const MyApp()); +} ``` -To enable browser credentials for CORS or use platform-native HTTP clients, see [Configure HTTP calls](./endpoints-and-apis/configure-http-calls). +The `getServerUrl()` helper from `serverpod_flutter` picks the server address from the first of these that is set: -## Point the client at each environment +1. The `SERVER_URL` compile-time define, for [choosing the URL per build](#point-the-client-at-each-environment). +2. The `apiUrl` value in `assets/config.json`. +3. The default, `http://localhost:8080/`, where localhost adapts to the platform, so an Android emulator reaches your machine through `10.0.2.2` without any changes. -The URL you pass to `Client` is the server the app connects to, so it changes between development, staging, and production. Keep one client setup and choose the URL at build time with `--dart-define`: +The `connectivityMonitor` lets the client observe network changes. Projects created with authentication also attach an `authSessionManager` here. -```dart -const serverUrl = String.fromEnvironment( - 'SERVERPOD_URL', - defaultValue: 'http://localhost:8080/', -); +On a physical device, localhost is the device itself, so the app needs your machine's network address instead: -var client = Client(serverUrl) - ..connectivityMonitor = FlutterConnectivityMonitor(); -``` +1. Find your machine's LAN IP, with `ifconfig` on Linux/macOS or `ipconfig` on Windows. +2. Set it as the server URL, either as `apiUrl` in `assets/config.json` or with `--dart-define=SERVER_URL=http://192.168.1.20:8080/`. + +Also set `publicHost` in the development config to the same IP, so URLs the server hands out, such as public file links, point somewhere the device can reach; see [Configuration](./server-fundamentals/configuration). + +To enable browser credentials for CORS or use platform-native HTTP clients, see [Configure HTTP calls](./endpoints-and-apis/configure-http-calls). + +## Point the client at each environment -Pass the URL for each build: +The resolved server URL is the server the app connects to, so it changes between development, staging, and production. Keep the scaffolded client setup and choose the URL per build with `--dart-define`: ```bash -flutter run --dart-define=SERVERPOD_URL=https://staging.example.com/ +flutter run --dart-define=SERVER_URL=https://staging.example.com/ ``` -Without the define, the app falls back to the local server. In production, use your deployed server's public URL. If you deploy to [Serverpod Cloud](/cloud), that is the URL of your deployed app. +Without the define, the app falls back to `assets/config.json` and then the local server. For a Flutter web app served by your Serverpod web server, the server provides `config.json` at runtime with its own API address, so the deployed web app finds the right server without a rebuild. In production, use your deployed server's public URL. If you deploy to [Serverpod Cloud](/cloud), that is the URL of your deployed app. -## Passing parameters +## Restrict access to your endpoints -There are some limitations to how endpoint methods can be implemented. Parameters and return types can be of type `bool`, `int`, `double`, `String`, `UuidValue`, `Duration`, `DateTime`, `ByteData`, `Uri`, `BigInt`, or generated serializable objects (see next section). A typed `Future` should always be returned. Null safety is supported. When passing a `DateTime` it is always converted to UTC. +Endpoints are open to any client by default. To require a signed-in user, override `requireLogin` and to require specific scopes, override `requiredScopes`: -You can also pass `List`, `Map`, `Record`, and `Set` as parameters, but they need to be strictly typed with one of the types mentioned above. +```dart +class PrivateEndpoint extends Endpoint { + @override + bool get requireLogin => true; -:::warning + Future onlyForSignedInUsers(Session session) async { + return 'Hello ${session.authenticated?.userIdentifier}'; + } +} +``` -While it's possible to pass binary data through a method call and `ByteData`, it is not the most efficient way to transfer large files. See our [file upload](./endpoints-and-apis/file-uploads) interface. The size of a call is by default limited to 524288 bytes (512 KiB). It's possible to change by adding the `maxRequestSize` to your config files. E.g., this will double the request size to 1 MiB: +If a call fails the check, the app receives a typed error; see [error handling](./endpoints-and-apis/error-handling-and-exceptions). -```yaml -maxRequestSize: 1048576 -``` +For how users sign in and how scopes work, see [Authentication](./authentication/basics). -::: +## Pass and return data -## Return types +Parameters and return values can be of type `bool`, `int`, `double`, `String`, `UuidValue`, `Duration`, `DateTime`, `ByteData`, `Uri`, `BigInt`, `dynamic`, [vector and geography types](./data-and-the-database/models/vector-and-geography-fields), or generated serializable objects ([data models](./data-and-the-database/models)). You can also use `List`, `Map`, `Record`, and `Set`, strictly typed with the types above. Null safety is supported, and return types follow the same rules as parameters. A `DateTime` is always converted to UTC when passed. -The return type must be a typed Future. Supported return types are the same as for parameters. +Parameters are sent by name in the request between app and server, so renaming an endpoint method's parameters breaks older app builds. See [backward compatibility](./data-and-the-database/models/backward-compatibility) before changing a published API. -## Excluding endpoints from code generation +Binary data over a method call is capped by the request size limit, 512 KiB by default (an oversized call fails with an HTTP 413 error). The limit is the `maxRequestSize` key in the [Configuration reference](./lookups/configuration-reference). For transferring files, use the [file upload](./endpoints-and-apis/file-uploads) interface instead. -If you want the code generator to ignore an endpoint definition, you can annotate either the entire class or individual methods with `@doNotGenerate`. This can be useful if you want to keep the definition in your codebase without generating server or client bindings for it. +## Exclude an endpoint from generation -### Exclude an entire `Endpoint` class +If you want the code generator to ignore an endpoint definition, annotate either the entire class or individual methods with `@doNotGenerate`. This is useful for keeping a definition in your codebase without generating server or client bindings for it. -Annotate the class with `@doNotGenerate` to exclude it entirely: +### Exclude an entire `Endpoint` class ```dart import 'package:serverpod/serverpod.dart'; @@ -133,12 +126,10 @@ class ExampleEndpoint extends Endpoint { } ``` -The above code will not generate any server or client bindings for the example endpoint. +The above code generates no server or client bindings for the example endpoint. ### Exclude individual `Endpoint` methods -Alternatively, you can exclude individual methods by annotating them with `@doNotGenerate`. - ```dart import 'package:serverpod/serverpod.dart'; @@ -154,4 +145,20 @@ class ExampleEndpoint extends Endpoint { } ``` -In this case the `ExampleEndpoint` will only expose the `hello` method, whereas the `goodbye` method will not be accessible externally. +In this case the `ExampleEndpoint` only exposes the `hello` method, and the `goodbye` method is not accessible externally. For how the annotation interacts with inheritance, see [Endpoint inheritance](./endpoints-and-apis/endpoint-inheritance). + +## Test your endpoints + +The generated test tools call your endpoints the same way production code does, against a test database. See [Get started with testing](./testing/get-started). + +## Related + +- [Sessions](./endpoints-and-apis/sessions): the object every endpoint method receives. +- [Error handling and exceptions](./endpoints-and-apis/error-handling-and-exceptions): typed errors across the wire. +- [Streaming](./endpoints-and-apis/streaming): push live data to your app. +- [File uploads](./endpoints-and-apis/file-uploads): direct-to-storage uploads. +- [Endpoint inheritance](./endpoints-and-apis/endpoint-inheritance): share behavior across endpoints and reshape module endpoints. +- [Server events](./endpoints-and-apis/server-events): publish and subscribe to messages across sessions and servers. +- [Endpoint middleware](./endpoints-and-apis/endpoint-middleware): wrap every API request for logging or rate limiting. +- [Configure HTTP calls](./endpoints-and-apis/configure-http-calls): CORS credentials and native HTTP stacks. +- [Working with models](./data-and-the-database/models): define the data your endpoints exchange. diff --git a/docs/06-concepts/02-endpoints-and-apis/02-sessions.md b/docs/06-concepts/02-endpoints-and-apis/02-sessions.md index 9cc937bd..4109e674 100644 --- a/docs/06-concepts/02-endpoints-and-apis/02-sessions.md +++ b/docs/06-concepts/02-endpoints-and-apis/02-sessions.md @@ -4,7 +4,7 @@ description: The Session object gives every endpoint call access to the database # Sessions -Every endpoint method receives a `Session` object. It is the entry point for the database, cache, file storage, and messaging system. Serverpod creates and closes sessions automatically for endpoint calls; for background tasks you create and close them manually. +Every endpoint method receives a `Session` object. It is the entry point for the database, cache, file storage, and messaging system. Serverpod creates and closes sessions automatically for endpoint calls. For background work, you create one yourself, most easily with [`withSession`](#create-a-session-for-background-work). :::note @@ -12,93 +12,57 @@ A Serverpod Session should not be confused with the concept of "web sessions" or ::: -## Quick reference - -### Essential properties - -- **`db`** - Database access. [See database docs](../data-and-the-database/database/connection) -- **`caches`** - Local and distributed caching. [See caching docs](../operations/caching) -- **`storage`** - File storage operations. [See file uploads](./file-uploads) -- **`messages`** - Server events for real-time communication within and across servers. [See server events docs](./server-events) -- **`passwords`** - Credentials from config and environment. [See configuration](../server-fundamentals/configuration) -- **`authenticated`** - Current user authentication info. [See authentication docs](../authentication/basics) - -### Key methods - -- **`log(message, level)`** - Add log entry -- **`addWillCloseListener(callback)`** - Register cleanup callback - ## Session types -Serverpod creates different session types based on the context: +Serverpod creates a session for every unit of work it runs, and the type reflects what triggered it. You will see these names in your [logs](../operations/logging) and in Insights when tracing a call: -| Type | Created For | Lifetime | Common Use Cases | -| ----------------------- | ---------------------------- | ------------------- | ---------------------------- | -| **MethodCallSession** | `Future` endpoint methods | Single request | API calls, CRUD operations | -| **WebCallSession** | Web server routes | Single request | Web pages, form submissions | -| **MethodStreamSession** | `Stream` endpoint methods | Stream duration | Real-time updates, chat | -| **StreamingSession** | WebSocket connections | Connection duration | Live dashboards, multiplayer | -| **FutureCallSession** | Scheduled tasks | Task execution | Email sending, batch jobs | -| **InternalSession** | Manual creation | Until closed | Background work, migrations | +| Type | Created for | Lifetime | Typical uses | +| ----------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------- | ---------------------------- | +| **MethodCallSession** | `Future` [endpoint methods](../endpoints-and-apis) | Single request | API calls, CRUD operations | +| **WebCallSession** | [Web server](../web-server/overview) routes | Single request | Web pages, form submissions | +| **MethodStreamSession** | [Streaming methods](./streaming) | Stream duration | Real-time updates, chat | +| **StreamingSession** | WebSocket connections of the [deprecated streaming endpoints API](./streaming#streaming-endpoints-deprecated) | Connection duration | Legacy real-time code | +| **FutureCallSession** | [Scheduled tasks](../scheduling/setup) | Task execution | Email sending, batch jobs | +| **InternalSession** | [Manual creation](#create-a-session-for-background-work) | Until closed | Background work, migrations | -### Example: Automatic session (MethodCallSession) +You rarely choose a type yourself: your endpoint methods receive the right one, and manual creation always produces an `InternalSession`. ```dart -// lib/src/endpoints/example_endpoint.dart class ExampleEndpoint extends Endpoint { Future hello(Session session, String name) async { - // MethodCallSession is created automatically + // A MethodCallSession was created for this call, and it closes + // automatically when the method returns. return 'Hello $name'; - // Session closes automatically when method returns } } ``` -### Example: Manual session (InternalSession) - -InternalSession is the only session type that requires manual management: - -```dart -Future performMaintenance() async { - var session = await Serverpod.instance.createSession(); - try { - // Perform operations - await cleanupOldRecords(session); - await updateStatistics(session); - } finally { - await session.close(); // Must close manually! - } -} -``` +## What a session provides -**Important**: Always use try-finally with InternalSession to prevent memory leaks. +| Member | What it is | +| --------------------------- | ----------------------------------------------------------------------------------------------- | +| `db` | Database access. See the [database docs](../data-and-the-database/database/connection). | +| `caches` | Local and distributed caching. See [caching](../operations/caching). | +| `storage` | File storage. See [file uploads](./file-uploads). | +| `messages` | Server events for real-time communication. See [server events](./server-events). | +| `passwords` | Secrets from config and environment. See [configuration](../server-fundamentals/configuration). | +| `authenticated` | The current user's authentication info. See [authentication](../authentication/basics). | +| `log(...)` | Write a log entry tied to this session. See [logging](#logging). | +| `addWillCloseListener(...)` | Register a cleanup callback that runs before the session closes. | +| `serverpod` | The running Serverpod instance, for example for [future calls](../scheduling/setup). | ## Session lifecycle -```mermaid -flowchart TB - Request([Request/Trigger]) --> Create[Session Created] - Create --> Init[Initialize] - Init --> Active[Execute Method] - Active --> Close[Close Session] - Close --> End([Request Complete]) -``` - -When a client makes a request, Serverpod creates the appropriate session type (see table above) and tears it down when the operation completes. Most sessions close automatically; `InternalSession` is the exception. - -### Internal Sessions - -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`. +Serverpod creates the session when the request or task starts and closes it when the work completes. Closing finalizes the session's log and releases its resources. The exception is `InternalSession`: you [create it yourself](#create-a-session-for-background-work), so you close it yourself. -### Session cleanup callbacks +### Run cleanup when a session closes -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: +Register callbacks that execute just before a session closes with `addWillCloseListener`. This is useful for releasing resources or performing final operations: ```dart Future processData(Session session) async { var tempFile = File('/tmp/processing_data.tmp'); - // Register cleanup callback session.addWillCloseListener((session) async { if (await tempFile.exists()) { await tempFile.delete(); @@ -106,198 +70,82 @@ Future processData(Session session) async { } }); - // Process data using temp file await tempFile.writeAsString('processing...'); - // Session closes automatically, cleanup callback runs + // The session closes automatically after the method returns, + // and the cleanup callback runs. } ``` -Cleanup callbacks run in the order they were registered and are called for all session types, including manual sessions when you call `session.close()`. - -## Logging - -Log entries are written to the database when the session closes. Streaming sessions (`MethodStreamSession` and `StreamingSession`) write logs continuously to avoid memory buildup during long connections. - -:::warning - -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. - -::: - -## Common pitfalls and solutions - -### Pitfall 1: Using session after method returns - -**Problem:** Using a session after it's closed throws a `StateError` - -```dart -Future processUser(Session session, int userId) async { - var user = await User.db.findById(session, userId); - - // Schedule async work - Timer(Duration(seconds: 5), () async { - // ❌ Session is already closed! - // This will throw: StateError: Session is closed - await user.updateLastSeen(session); - }); - - return; // Session closes here -} -``` - -**Solution 1 - Use FutureCalls:** - -```dart -Future processUser(Session session, int userId) async { - var user = await User.db.findById(session, userId); - - // Schedule through Serverpod - await session.serverpod.futureCallWithDelay( - 'updateLastSeen', - UserIdData(userId: userId), - Duration(seconds: 5), - ); +Cleanup callbacks run in the order they were registered and fire for all session types, including manual sessions when you call `session.close()`. - return; -} -``` +## Create a session for background work -**Solution 2 - Create manual session:** +Inside an endpoint method, use the session you were given. It is already managed for you. When code runs outside a call, such as startup work in `run()` or a background job, create a session with `withSession`, which builds an `InternalSession`, runs your callback, and closes the session afterwards: ```dart -Future processUser(Session session, int userId) async { - var user = await User.db.findById(session, userId); - - Timer(Duration(seconds: 5), () async { - // Create new session for async work - var newSession = await Serverpod.instance.createSession(); - try { - await user.updateLastSeen(newSession); - } finally { - await newSession.close(); - } - }); - - return; -} +await pod.withSession((session) async { + await cleanupOldRecords(session); + await updateStatistics(session); +}); ``` -### Pitfall 2: Forgetting to close manual sessions +If the callback throws, the session is closed with the error and stack trace attached, so they reach the logs, and the error is rethrown. -**Problem:** - -```dart -// ❌ Memory leak! -var session = await Serverpod.instance.createSession(); -var users = await User.db.find(session); -// Forgot to close - session leaks memory -``` - -**Solution - Always use try-finally:** +If you need to manage the lifetime yourself, create the session manually and close it when done. An unclosed session is never finalized and its resources are not released, so pair `createSession` with a `finally`: ```dart var session = await Serverpod.instance.createSession(); try { var users = await User.db.find(session); - // Process users + // Process users. } finally { - await session.close(); // Always runs + await session.close(); } ``` -## Best practices +## Run work after the call returns -### 1. Let Serverpod manage sessions when possible - -Prefer using the session provided to your endpoint rather than creating new ones: +The session that an endpoint method receives closes when the method returns, so do not capture it in timers or delayed callbacks: ```dart -// ✅ Good - Use provided session -Future> getActiveUsers(Session session) async { - return await User.db.find( - session, - where: (t) => t.isActive.equals(true), - ); -} - -// ❌ Avoid - Creating unnecessary session -Future> getActiveUsers(Session session) async { - var newSession = await Serverpod.instance.createSession(); - try { - return await User.db.find(newSession, ...); - } finally { - await newSession.close(); - } +Future processUser(Session session, int userId) async { + Timer(Duration(seconds: 5), () async { + // Do not do this: this session closed when processUser returned, + // and this callback runs outside any managed lifetime. + await updateLastSeen(session, userId); + }); } ``` -### 2. Use FutureCalls for delayed operations - -Instead of managing sessions for async work, use Serverpod's [future call system](../scheduling/setup): - -```dart -// ✅ Good - Let Serverpod manage the session -await serverpod.futureCallWithDelay( - 'processPayment', - PaymentData(orderId: order.id), - Duration(hours: 1), -); - -// ❌ Complex - Manual session management -Future.delayed(Duration(hours: 1), () async { - var session = await Serverpod.instance.createSession(); - try { - await processPayment(session, order.id); - } finally { - await session.close(); - } -}); -``` - -### 3. Handle errors properly - -Always handle exceptions to prevent unclosed sessions: +Schedule the work as a [future call](../scheduling/setup) instead. Future calls survive server restarts and receive their own session when they run: ```dart -// ✅ Good - Errors won't prevent session cleanup -Future safeOperation() async { - var session = await Serverpod.instance.createSession(); - try { - await riskyOperation(session); - } catch (e) { - session.log('Operation failed: $e', level: LogLevel.error); - // Handle error appropriately - } finally { - await session.close(); - } +Future processUser(Session session, int userId) async { + await session.serverpod.futureCalls + .callWithDelay(const Duration(seconds: 5)) + .userMaintenance + .updateLastSeen(userId); } ``` -## Testing +For one-off work that can run immediately and does not need to survive a restart, `withSession` inside the callback is the lighter alternative. -When testing endpoints, the `TestSessionBuilder` can be used to simulate and configure session properties for controlled test scenarios: +## Logging + +Use `session.log` to write log entries tied to the session. Entries are persisted according to your [session log configuration](../operations/logging), and closing the session finalizes its log. Sessions for [streaming methods](./streaming) are the exception: they write entries continuously while the stream is open, so a long-lived stream appears in your logs before it ends. This is on by default in the runtime log settings. See [Logging](../operations/logging) for storage, retention, and console output. ```dart -withServerpod('test group', (sessionBuilder, endpoints) { - test('endpoint test', () async { - var result = await endpoints.users.getUser(sessionBuilder, 123); - expect(result.name, 'John'); - }); +session.log('Processing started'); +session.log('Something looks off', level: LogLevel.warning); +``` - test('authenticated endpoint test', () async { - const int userId = 1234; +## Test with sessions - var authenticatedSessionBuilder = sessionBuilder.copyWith( - authentication: AuthenticationOverride.authenticationInfo(userId, {Scope('user')}), - ); +The test tools provide a `sessionBuilder` for calling endpoints in tests and simulating session state such as authentication. See [Get started with testing](../testing/get-started) and [the basics](../testing/the-basics) for the patterns. - var result = await endpoints.users.updateProfile( - authenticatedSessionBuilder, - ProfileData(name: 'Jane') - ); - expect(result.success, isTrue); - }); -}); -``` +## Related -For detailed testing strategies, see the [testing documentation](../testing/get-started). +- [Working with endpoints](../endpoints-and-apis): the methods that receive sessions. +- [Scheduling](../scheduling/setup): future calls, the managed way to run delayed work. +- [Logging](../operations/logging): where session log entries go. +- [Authentication](../authentication/basics): what `session.authenticated` holds. diff --git a/docs/06-concepts/02-endpoints-and-apis/06-error-handling-and-exceptions.md b/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md similarity index 73% rename from docs/06-concepts/02-endpoints-and-apis/06-error-handling-and-exceptions.md rename to docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md index c455d3c2..c6a00f80 100644 --- a/docs/06-concepts/02-endpoints-and-apis/06-error-handling-and-exceptions.md +++ b/docs/06-concepts/02-endpoints-and-apis/03-error-handling-and-exceptions.md @@ -1,24 +1,20 @@ --- -description: Handle server errors in Serverpod by defining serializable exceptions that are thrown on the server and caught in your Flutter app. +description: Errors in Serverpod cross the wire as serializable exceptions defined in model files, caught by type in the app alongside typed HTTP failures. --- # Error handling and exceptions -Serverpod allows you to throw an exception on the server, serialize it, and catch it in your client app. +Errors on the server reach your app as typed Dart exceptions. You define an exception once, throw it on the server, and catch it by type in your Flutter app. Failures carry structured data instead of strings. If you throw a normal exception that isn't caught by your code, it will be treated as an internal server error. The exception will be logged together with its stack trace, and a 500 HTTP status (internal server error) will be sent to the client. On the client side, this throws a `ServerpodClientException` with status code 500 (specifically `ServerpodClientInternalServerError`). The error message and stack trace stay on the server, logged to the `serverpod_session_log` table. -:::tip -Use the Serverpod Insights app to view your logs. It will show any failed or slow calls and will make it easy to pinpoint any errors in your server. -::: - :::info -Uncaught exceptions thrown in endpoints are logged in the `serverpod_session_log` table, not in the `serverpod_log` table. To understand more about the differences between these two tables, you can read more about [logging](../operations/logging) in Serverpod. +Session logs live in `serverpod_session_log`, not `serverpod_log`; see [logging](../operations/logging) for the difference. The Serverpod Insights app shows failed and slow calls. ::: ## Serializable exceptions -Serverpod allows adding data to an exception you throw on the server and extracting that data in the client. You use the same YAML files to define the serializable exceptions as you would with any serializable model (see [serialization](../data-and-the-database/models/custom-serialization) for details). The only difference is that you use the keyword `exception` instead of `class`. +Serverpod allows adding data to an exception you throw on the server and extracting that data in the client. You define them in the same model files as any serializable model (see [Working with models](../data-and-the-database/models) for how model files work). The only difference is that you use the keyword `exception` instead of `class`. ```yaml exception: MyException @@ -27,7 +23,7 @@ fields: errorType: MyEnum ``` -After you run `serverpod generate`, you can throw that exception when processing a call to the server. +Once the code is generated, you can throw that exception when processing a call to the server. ```dart class ExampleEndpoint extends Endpoint { @@ -104,10 +100,10 @@ The `SerializableException` interface marks the exception as safe to serialize t ### Default values in exceptions -Serverpod allows you to specify default values for fields in exceptions, similar to how it's done in models using the `default` and `defaultModel` keywords. If you're unfamiliar with how these keywords work, you can refer to the [Default Values](../data-and-the-database/models#default-values) section in the [Working with Models](../data-and-the-database/models) documentation. +Serverpod allows you to specify default values for fields in exceptions, similar to models: `default` sets the value when the field is omitted, and `defaultModel` sets it on the Dart side. See [default values](../data-and-the-database/models#default-values) in Working with models. :::info -Since exceptions are not persisted in the database, the `defaultPersist` keyword is not supported. If both `default` and `defaultModel` are specified, `defaultModel` will always take precedence, making it unnecessary to use both. +Since exceptions are not persisted in the database, the `defaultPersist` keyword is not supported. If both `default` and `defaultModel` are specified, `defaultModel` takes precedence. ::: ```yaml @@ -117,13 +113,15 @@ fields: errorCode: int, default=1001 ``` -## Handling errors on the client +## Handle errors in your app A call from the client can fail in three ways, and you usually handle each one differently: -- A **serializable exception you defined** (`MyException` above): a known, app-level failure. Catch it by its type and show the reader what happened. +- A **serializable exception you defined** (`MyException` above): a known, app-level failure. Catch it by its type and show the user what happened. (On the wire, it travels as an HTTP 400 with a typed payload.) - A **`ServerpodClientException`**: something went wrong in the communication or on the server. Its typed subclasses map to HTTP status codes: `ServerpodClientBadRequest` (400), `ServerpodClientUnauthorized` (401), `ServerpodClientForbidden` (403), `ServerpodClientNotFound` (404), and `ServerpodClientInternalServerError` (500). -- A **connection failure**: when the app cannot reach the server (offline, wrong URL, or a timeout), it throws a `ServerpodClientException` with a `statusCode` of `-1`. +- A **connection failure**: when the app cannot reach the server (offline, wrong URL, or a timeout), it throws a `ServerpodClientException` with a `statusCode` of `-1`. A call that exceeds the [request size limit](../endpoints-and-apis#pass-and-return-data) fails with a generic `ServerpodClientException` with status code 413. + +Calls to [streaming methods](./streaming) fail with their own connection-level exception family; see [error handling in streams](./streaming#error-handling). Catch the specific cases first, then fall back to the general one: @@ -151,6 +149,6 @@ try { Only the serializable exceptions you define reach the client, and every field on them is sent as-is. An uncaught exception becomes a generic 500 with no details, so internal errors never leak on their own. The risk is what you put on the exceptions you do send. -- Don't put stack traces, secrets, database IDs, or internal messages into serializable exception fields. Send only what the reader should see. +- Don't put stack traces, secrets, database IDs, or internal messages into serializable exception fields. Send only what the user should see. - Write user-facing messages, and keep the diagnostic detail in your server logs where you can look it up later. - Validate and sanitize input before acting on it, so a bad request fails cleanly instead of surfacing an internal error. diff --git a/docs/06-concepts/02-endpoints-and-apis/04-streaming.md b/docs/06-concepts/02-endpoints-and-apis/04-streaming.md new file mode 100644 index 00000000..ebb83489 --- /dev/null +++ b/docs/06-concepts/02-endpoints-and-apis/04-streaming.md @@ -0,0 +1,196 @@ +--- +description: Streaming methods in Serverpod are endpoint methods that return or receive Dart Streams over a managed WebSocket, with typed exceptions across the wire. +--- + +# Streaming + +Some features need the server to push data to the app the moment it changes: chat, multiplayer games, live dashboards. Streaming methods cover this: an endpoint method that returns or receives a Dart `Stream`, transmitted over a shared WebSocket connection that Serverpod manages for you. + +:::tip + +For a real-world example, check out [Pixorama](https://pixorama.live). It's a multi-user drawing experience showcasing Serverpod's real-time capabilities and comes with complete source code. + +::: + +## Streaming methods + +When an endpoint method is defined with `Stream` instead of `Future` as the return type, or includes `Stream` as a method parameter, it is recognized as a streaming method. + +### Define a streaming method + +Streaming methods are defined by using the `Stream` type as either the return value or a parameter. + +Following is an example of a streaming method that echoes back any message: + +```dart +class ExampleEndpoint extends Endpoint { + Stream echoStream(Session session, Stream stream) async* { + await for (var message in stream) { + yield message; + } + } +} +``` + +The `Stream` can also be given a type argument, such as `Stream`. The type carries into the generated client, so the compiler checks the messages you send and receive. + +The streaming method above can then be called from the client like this: + +```dart +var inStream = StreamController(); +var outStream = client.example.echoStream(inStream.stream); +outStream.listen((message) { + print('Received message: $message'); +}); + +inStream.add('Hello'); +inStream.add(42); + +// This will print +// Received message: Hello +// Received message: 42 +``` + +In the example above, the `echoStream` method reads each message from its stream parameter and sends it back on its return stream. + +:::tip + +The stream is defined as dynamic and can contain any type that can be serialized by Serverpod. + +::: + +### Lifecycle of a streaming method + +Each time the client calls a streaming method, a new [`Session`](./sessions) is created for the call, and it closes automatically when the call is over. If the WebSocket connection is lost, all streaming methods are closed on both the server and the client. + +How long the call stays alive depends on the method's shape: + +- A method returning a `Stream` is kept alive until the client cancels its subscription or the method returns. +- A method returning a `Future` (with `Stream` parameters) is kept alive until the method returns. + +A stream parameter closes when the client closes the stream or the server cancels its subscription to it, and all stream parameters close when the call ends. + +### Authentication + +Authentication is integrated into streaming method calls. When a client initiates a streaming method, the server automatically authenticates the session. + +Authentication is validated when the stream is first established, using the authentication data stored in the `Session` object. If a user's authentication is revoked, the stream is closed and an exception is thrown. + +For more details on handling revoked authentication, refer to the section on [handling revoked authentication](../authentication/custom-overrides#handling-revoked-authentication). + +### WebSocket ping interval + +The server sends periodic ping messages on open streaming connections to keep them alive. The interval between pings is configurable and defaults to 30 seconds. + +If you deploy behind a load balancer or proxy with a shorter idle timeout (for example, 15-20 seconds), you may need to lower the ping interval so connections are not closed. Set the `SERVERPOD_WEBSOCKET_PING_INTERVAL` environment variable to the desired interval in seconds, or configure `websocketPingInterval` in your config file; see the [Configuration reference](../lookups/configuration-reference). + +### Error handling + +A streaming call can fail in two ways. Errors your code raises travel over the stream as [serializable exceptions](./error-handling-and-exceptions). Failures in the connection itself throw a [`MethodStreamException` subtype](#connection-level-exceptions) instead. + +If an exception is thrown on a stream, the stream is closed with an exception. If the thrown exception is serializable, it is serialized and delivered over the stream before the stream closes, in both directions: stream parameters can pass exceptions to the server, and return streams can pass exceptions to the client. + +Define the exception in a model file, like any [serializable exception](./error-handling-and-exceptions): + +```yaml +exception: CountdownException +fields: + message: String +``` + +Throw it from the streaming method, and catch it on the stream in your app: + +```dart +class ExampleEndpoint extends Endpoint { + Stream countdown(Session session) async* { + yield 3; + yield 2; + throw CountdownException(message: 'Countdown aborted'); + } +} +``` + +```dart +var stream = client.example.countdown(); +stream.listen((number) { + print('Countdown: $number'); +}, onError: (error) { + if (error is CountdownException) { + print('Countdown failed: ${error.message}'); + } +}); +``` + +In the other direction, add the error to a stream parameter on the client, and catch it typed on the server around the `await for`: + +```dart +Future reportErrors(Session session, Stream values) async { + try { + await for (final _ in values) {} + return 'completed without error'; + } on CountdownException catch (e) { + return 'caught: ${e.message}'; + } +} +``` + +```dart +var controller = StreamController(); +var result = client.example.reportErrors(controller.stream); +controller.addError(CountdownException(message: 'Error from client')); +await controller.close(); +print(await result); // caught: Error from client +``` + +### Connection-level exceptions + +When the failure is in the connection rather than your code, the client throws a subtype of `MethodStreamException`: + +| Exception | When it is thrown | +| ------------------------------------ | ----------------------------------------------------------------------------- | +| `WebSocketConnectException` | The WebSocket connection to the server could not be opened. | +| `ConnectionAttemptTimedOutException` | Opening the connection did not complete in time. | +| `WebSocketListenException` | Listening on the WebSocket failed after it was opened. | +| `WebSocketClosedException` | The WebSocket closed while the stream was in use. | +| `OpenMethodStreamException` | The server declined to open the stream; carries a reason: `endpointNotFound`, `authenticationFailed`, `authorizationDeclined`, or `invalidArguments`. | +| `ConnectionClosedException` | The stream connection closed, for example when authentication was revoked. | +| `MethodStreamIdleTimeoutException` | The stream was idle past the client's idle timeout. | + +Catch `MethodStreamException` to handle them as one group, or match specific subtypes when the reaction differs, for example prompting sign-in on `OpenMethodStreamException` with `authenticationFailed`. + +## Example: live updates for a filtered query + +A common real-time need is to push updates for one record or filter, for example the messages in a single chat room. Combine a streaming method with [server events](./server-events): the streaming method subscribes the client to a channel scoped to that filter, and whatever changes the data posts to the same channel. + +```dart +class ChatEndpoint extends Endpoint { + // The client subscribes to live messages for one room. + Stream watchRoom(Session session, int roomId) async* { + yield* session.messages.createStream('room_$roomId'); + } + + // Posting a message stores it and broadcasts it to everyone watching the room. + Future postToRoom(Session session, int roomId, String text) async { + var message = ChatMessage(roomId: roomId, text: text); + await ChatMessage.db.insertRow(session, message); + await session.messages.postMessage('room_$roomId', message); + } +} +``` + +On the client, listen to the returned stream and call `postToRoom` to publish: + +```dart +var messages = client.chat.watchRoom(roomId); +messages.listen((message) { + print('Room ${message.roomId}: ${message.text}'); +}); + +await client.chat.postToRoom(roomId, 'Hello, room!'); +``` + +Because the channel name includes the `roomId`, each client receives updates only for the room it is watching. The same pattern works for any filter: scope the channel by the id or query you care about, and post to it whenever the data changes. To fan the updates out across multiple server instances, post with `global: true` (see [Global messages](./server-events#global-messages)). + +## Streaming endpoints (deprecated) + +Serverpod's original streaming API (`streamOpened`, `handleStreamMessage`, `sendStreamMessage`, `openStreamingConnection`) is deprecated and will be removed in a future version. Use [streaming methods](#streaming-methods) instead. The old API is kept for reference in [Streaming endpoints (deprecated)](../../upgrading/archive/streaming-endpoints). diff --git a/docs/06-concepts/02-endpoints-and-apis/05-endpoint-middleware.md b/docs/06-concepts/02-endpoints-and-apis/05-endpoint-middleware.md deleted file mode 100644 index f479312a..00000000 --- a/docs/06-concepts/02-endpoints-and-apis/05-endpoint-middleware.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -sidebar_label: Endpoint middleware -description: Middleware on the API server intercepts endpoint requests and responses for concerns such as logging, caching, and rate limiting. ---- - -# Endpoint middleware - -Middleware runs before and after your endpoints, making it suitable for logging, caching, and rate limiting. Serverpod middleware follows the [Relic middleware](https://docs.dartrelic.dev/reference/middleware) interface. To add middleware to web server routes instead, scoped to path prefixes, see [Web server middleware](../web-server/web-server-middleware). - -## Adding middleware to your server - -Add middleware to your server in the `run` function before starting the server: - -```dart -import 'package:serverpod/serverpod.dart'; - -void run(List args) async { - final pod = Serverpod( - args, - Protocol(), - Endpoints(), - ); - - // Add middleware before starting - pod.server.addMiddleware(myCustomMiddleware()); - - await pod.start(); -} -``` - -## Creating custom middleware - -### Middleware signature - -A middleware is a function with this signature: - -```dart -typedef Handler = FutureOr Function(Request request); -typedef Middleware = Handler Function(Handler innerHandler); -``` - -The return value is a `Result`, which can be a `Response`, `Hijack`, or `WebSocketUpgrade`. Check `is Response` before modifying response-specific fields. - -### Simple middleware example - -Here's a basic middleware that adds a custom header to all responses: - -```dart -Middleware customHeaderMiddleware() { - return (Handler innerHandler) { - return (Request req) async { - // Inspect or modify `req` here if needed - - // Call the inner handler to process the request - final result = await innerHandler(req); - - // Modify the response - if (result is Response) { - return result.copyWith( - headers: result.headers.transform((h) { - h['X-Custom-Header'] = ['my-value']; - }), - ); - } - - return result; - }; - }; -} -``` - -## Error handling - -Middleware can catch and handle errors: - -```dart -Middleware errorHandlingMiddleware() { - return (Handler innerHandler) { - return (Request req) async { - try { - return await innerHandler(req); - } catch (e, stackTrace) { - // Log the error - print('Error processing request: $e'); - print('Stack trace: $stackTrace'); - - // Re-throw to let Serverpod handle it - rethrow; - } - }; - }; -} -``` - -Middleware runs on every request in the order it is added, so keep each one focused and efficient. diff --git a/docs/06-concepts/02-endpoints-and-apis/05-server-events.md b/docs/06-concepts/02-endpoints-and-apis/05-server-events.md new file mode 100644 index 00000000..032896a3 --- /dev/null +++ b/docs/06-concepts/02-endpoints-and-apis/05-server-events.md @@ -0,0 +1,98 @@ +--- +description: Server events in Serverpod post messages to named channels through session.messages, delivered locally or across a cluster via Redis. +--- + +# Server events + +Serverpod has a built-in event system: post a message to a named channel, and every listener on that channel receives it, even on other servers in a cluster. It is how one user's action reaches everyone else, like the chat-room updates in [streaming's live example](./streaming#example-live-updates-for-a-filtered-query). You access it through `session.messages`. + +## Send messages + +Send a message with the `postMessage` method. The message is published to the specified channel and must be a [data model](../data-and-the-database/models). + +```dart +var message = UserUpdate(); // Model that represents changes to user data. +await session.messages.postMessage('user_updates', message); +``` + +In the example above, the message is published on the `user_updates` channel. Any subscriber to this channel in the server will receive the message. + +### Global messages + +Serverpod uses Redis to pass messages between servers. To send a message to another server, [enable Redis](../server-fundamentals/configuration) and then set the `global` parameter to `true` when posting a message. + +```dart +var message = UserUpdate(); // Model that represents changes to user data. +await session.messages.postMessage('user_updates', message, global: true); +``` + +In the example above, the message is published to the `user_updates` channel and will be received by all servers connected to the same Redis instance. + +:::warning + +If Redis is not enabled, sending global messages throws a `StateError`. + +::: + +## Receive messages + +Serverpod provides two ways to handle incoming messages: by creating a stream that subscribes to a channel or by adding a listener to a channel. + +### Create a stream + +To create a stream that subscribes to a channel, use the `createStream` method. The stream emits a value whenever a message is posted to the channel. The typed variant, `createStream(...)`, emits an error if a posted message does not match the type. + +```dart +var stream = session.messages.createStream('user_updates'); +stream.listen((message) { + print('Received message: $message'); +}); +``` + +In the above example, a stream is created that listens to the `user_updates` channel and processes incoming messages. + +#### Stream lifecycle + +The stream is automatically closed when the session is closed. To close the stream manually, cancel the stream subscription. + +```dart +var stream = session.messages.createStream('user_updates'); +var subscription = stream.listen((message) { + print('Received message: $message'); +}); + +subscription.cancel(); +``` + +### Add a listener + +To add a listener to a channel, use the `addListener` method. The listener will be called whenever a message is posted to the channel. + +```dart +session.messages.addListener('user_updates', (message) { + print('Received message: $message'); +}); +``` + +In the above example, the listener will be called whenever a message is posted to the `user_updates` channel. Listeners receive both local and global messages. + +#### Listener lifecycle + +The listener is automatically removed when the session is closed. To manually remove a listener, use the `removeListener` method. + +```dart +var myListenerCallback = (message) { + print('Received message: $message'); +}; +// Register the listener +session.messages.addListener('user_updates', myListenerCallback); + +// Remove the listener +session.messages.removeListener('user_updates', myListenerCallback); +``` + +In the above example, the listener is first added and then removed from the `user_updates` channel. + +## Broadcast revoked authentication + +The messaging interface also carries authentication revocation events: `session.messages.authenticationRevoked(userIdentifier, message)` notifies every server that a user's authentication changed, falling back to local delivery when Redis is disabled. For when and how to call it, see [handling revoked authentication](../authentication/custom-overrides#handling-revoked-authentication). diff --git a/docs/06-concepts/02-endpoints-and-apis/09-file-uploads.md b/docs/06-concepts/02-endpoints-and-apis/06-file-uploads.md similarity index 72% rename from docs/06-concepts/02-endpoints-and-apis/09-file-uploads.md rename to docs/06-concepts/02-endpoints-and-apis/06-file-uploads.md index f2dba003..5ee60519 100644 --- a/docs/06-concepts/02-endpoints-and-apis/09-file-uploads.md +++ b/docs/06-concepts/02-endpoints-and-apis/06-file-uploads.md @@ -1,14 +1,14 @@ --- -description: File upload handling in Serverpod uses cloud storage providers (GCP, S3, Cloudflare R2) or the database, with server-generated upload descriptions and client-side verification. +description: File uploads in Serverpod go directly to storage via signed upload descriptions, with database, GCP, S3, and Cloudflare R2 backends. --- -# Uploading files +# File uploads -Serverpod has built-in support for handling file uploads. Out of the box, your server is configured to use the database for storing files. This works well for testing but may not be performant in larger-scale applications. You should set up your server to use Google Cloud Storage or S3 in production scenarios. +Let your users upload avatars, documents, or any other files. The app sends the file straight to storage instead of through your endpoint methods, which keeps large files out of your API calls. Out of the box, your server stores files in the database, which works well for development. In production, configure Google Cloud Storage, AWS S3, or Cloudflare R2 instead. -## How to upload a file +## Upload a file -A `public` and `private` file storage are set up by default to use the database. You can replace these or add more configurations for other file storages. +A `public` and a `private` file storage are set up by default. You can replace these or add more configurations for other file storages. ### Server-side code @@ -30,8 +30,10 @@ The `createDirectFileUploadDescription` method also accepts optional parameters - **`contentLength`** - The exact file size in bytes. When provided, the storage provider validates the upload size against `maxFileSize` and may enforce the exact size server-side (e.g. via signed URLs). - **`preventOverwrite`** - When `true`, the upload will fail if a file already exists at the given path. Defaults to `false`. +Whether the options are enforced depends on the provider: the GCP native storage supports `preventOverwrite`, while the HMAC-based interface ignores it. + ```dart -Future getUploadDescription( +Future getRestrictedUploadDescription( Session session, String path, int fileSize, @@ -46,7 +48,7 @@ Future getUploadDescription( } ``` -After the file is uploaded, you should verify that the upload has been completed. If you are uploading a file to a third-party service, such as S3 or Google Cloud Storage, there is no other way of knowing if the file was uploaded or if the upload was canceled. +After the file is uploaded, verify that the upload completed. With a third-party service such as S3 or Google Cloud Storage, this is the only way to know it was not canceled. ```dart Future verifyUpload(Session session, String path) async { @@ -59,20 +61,20 @@ Future verifyUpload(Session session, String path) async { ### Client-side code -To upload a file from the app side, first request the upload description. Next, upload the file. You can upload from either a `Stream` or a `ByteData` object. If you are uploading a larger file, using a `Stream` is better because not all of the data must be held in RAM. Finally, you should verify the upload with the server. +To upload a file from the app side, first request the upload description. Next, upload the file, from either a `Stream` or a `ByteData` object. When uploading from a `Stream`, pass the file length if you know it: without a length, a multipart upload buffers the whole file in memory. The uploader does not report upload progress. Finally, verify the upload with the server. ```dart var uploadDescription = await client.myEndpoint.getUploadDescription('myfile'); if (uploadDescription != null) { var uploader = FileUploader(uploadDescription); - await uploader.upload(myStream); + await uploader.upload(myStream, myFileLength); var success = await client.myEndpoint.verifyUpload('myfile'); } ``` :::info -In a real-world app, you most likely want to create the file paths on your server. For your file paths to be compatible with S3, do not use a leading slash; only use standard characters and numbers. E.g.: +In a real-world app, you most likely want to create the file paths on your server. For your file paths to be compatible with S3, do not use a leading slash. Only use standard characters and numbers. E.g.: ```dart 'profile/$userId/images/avatar.png' @@ -80,9 +82,9 @@ In a real-world app, you most likely want to create the file paths on your serve ::: -## Accessing stored files +## Access stored files -You can check if a file exists or retrieve it directly from your server. Files in public storage are also accessible via URL; private files can only be accessed from the server. +You can check if a file exists or retrieve it directly from your server. Files in public storage are also accessible via URL. Private files can only be accessed from the server. To check if a file exists, use the `fileExists` method. @@ -111,7 +113,7 @@ var myByteData = await session.storage.retrieveFile( ); ``` -To store a file directly from the server, use the `storeFile` method. You can set `preventOverwrite` to `true` to ensure the upload fails if a file already exists at the given path. +To store a file directly from the server, use the `storeFile` method. You can set `preventOverwrite` to `true` to ensure the write fails if a file already exists at the given path, and `expiration` to give the file an expiry time. ```dart await session.storage.storeFile( @@ -122,9 +124,9 @@ await session.storage.storeFile( ); ``` -## Configuring a storage provider +To delete a stored file, use `deleteFile` with the same `storageId` and `path`. To look up public URLs for many files at once, use `getPublicUrls`. -By default, Serverpod uses the database for file storage. This works well for testing but is not recommended for production. Instead, you should configure a cloud storage provider. Serverpod supports Google Cloud Storage, AWS S3, and Cloudflare R2. +## Configure a storage provider Each storage is identified by a `storageId`. Serverpod comes with two default storages, `public` and `private`. You can replace these with a cloud-backed implementation, or add additional storages with custom IDs. Register your cloud storage with `pod.addCloudStorage()` before starting the server. @@ -144,7 +146,7 @@ You may also want to add the bucket as a backend for your load balancer to give When you have set up your GCP bucket, you need to configure it in Serverpod. Add the GCP package to your `pubspec.yaml` file and import it in your `server.dart` file. ```bash -$ dart pub add serverpod_cloud_storage_gcp +dart pub add serverpod_cloud_storage_gcp ``` ```dart @@ -152,7 +154,7 @@ import 'package:serverpod_cloud_storage_gcp/serverpod_cloud_storage_gcp.dart' as gcp; ``` -After creating your Serverpod, you add a storage configuration. If you want to replace the default `public` or `private` storages, set the `storageId` to `public` or `private`. Set the public host if you have configured your GCP bucket to be accessible on a custom domain through a load balancer. You should add the cloud storage before starting your pod. The `bucket` parameter refers to the GCP bucket name (you can find it in the console) and the `publicHost` is the domain name used to access the bucket via https. +The `bucket` parameter is the GCP bucket name (find it in the console), and `publicHost` is the domain used to access the bucket over https when it sits behind a load balancer. ```dart pod.addCloudStorage( @@ -232,12 +234,12 @@ When using Application Default Credentials, the service account must have the `i ### AWS S3 -This section shows how to set up a storage using S3. Before you write your Dart code, you need to set up an S3 bucket. Most likely, you will also want to set up a CloudFront for the bucket, where you can use a custom domain and your own SSL certificate. Finally, you will need to get a set of AWS access keys and add them to your Serverpod password file (`AWSAccessKeyId` and `AWSAccessKey`) or pass them in as environment variables (`SERVERPOD_AWS_ACCESS_KEY_ID` and `SERVERPOD_AWS_ACCESS_KEY`). +This section shows how to set up a storage using S3. Before you write your Dart code, you need to set up an S3 bucket. Most likely, you will also want to set up a CloudFront for the bucket, where you can use a custom domain and your own SSL certificate. Finally, you will need to get a set of AWS access keys and add them to your Serverpod password file (`AWSAccessKeyId` and `AWSSecretKey`) or pass them in as environment variables (`SERVERPOD_AWS_ACCESS_KEY_ID` and `SERVERPOD_AWS_SECRET_KEY`). When you are all set with the AWS setup, include the S3 package in your `pubspec.yaml` file and import it in your `server.dart` file. ```bash -$ dart pub add serverpod_cloud_storage_s3 +dart pub add serverpod_cloud_storage_s3 ``` ```dart @@ -245,7 +247,7 @@ import 'package:serverpod_cloud_storage_s3/serverpod_cloud_storage_s3.dart' as s3; ``` -After creating your Serverpod, you add a storage configuration. If you want to replace the default `public` or `private` storages, set the `storageId` to `public` or `private`. Set the public host if you have configured your S3 bucket to be accessible on a custom domain through CloudFront. You should add the cloud storage before starting your pod. +Set `publicHost` if your S3 bucket is accessible on a custom domain through CloudFront. ```dart pod.addCloudStorage( @@ -260,7 +262,7 @@ pod.addCloudStorage( ); ``` -For your S3 configuration to work, you will also need to add your AWS credentials to the `passwords.yaml` file. You create the access keys from your AWS console when signed in as the root user. +For your S3 configuration to work, you will also need to add your AWS credentials to the `passwords.yaml` file. Create the access keys in the AWS console for a dedicated IAM user whose access is limited to the bucket. Avoid root-user access keys. ```yaml shared: @@ -275,7 +277,7 @@ Serverpod supports Cloudflare R2 as a cloud storage provider. R2 is S3-compatibl Add the R2 package to your `pubspec.yaml` file and import it in your `server.dart` file. ```bash -$ dart pub add serverpod_cloud_storage_r2 +dart pub add serverpod_cloud_storage_r2 ``` ```dart @@ -307,9 +309,3 @@ shared: ``` You can also pass credentials via environment variables: `SERVERPOD_R2_ACCESS_KEY_ID` and `SERVERPOD_R2_SECRET_KEY`. - -:::info - -If you are using the GCP or AWS Terraform scripts that are created with your Serverpod project, the required GCP or S3 buckets will be created automatically. The scripts will also configure your load balancer or Cloudfront and the certificates needed to access the buckets securely. - -::: diff --git a/docs/06-concepts/02-endpoints-and-apis/03-endpoint-inheritance.md b/docs/06-concepts/02-endpoints-and-apis/07-endpoint-inheritance.md similarity index 90% rename from docs/06-concepts/02-endpoints-and-apis/03-endpoint-inheritance.md rename to docs/06-concepts/02-endpoints-and-apis/07-endpoint-inheritance.md index 9454b3fe..ee746db3 100644 --- a/docs/06-concepts/02-endpoints-and-apis/03-endpoint-inheritance.md +++ b/docs/06-concepts/02-endpoints-and-apis/07-endpoint-inheritance.md @@ -4,10 +4,9 @@ description: Endpoint inheritance lets one endpoint extend another, override beh # Endpoint inheritance -Endpoints can be based on other endpoints using inheritance, like `class ChildEndpoint extends ParentEndpoint`. If the parent endpoint was marked as `abstract` or `@doNotGenerate`, no client code is generated for it, but a client will be generated for your subclass, as long as it does not opt out again. -Inheritance gives you the possibility to modify the behavior of `Endpoint` classes defined in other Serverpod modules. +An endpoint can extend another endpoint, declared like any Dart class: `class ChildEndpoint extends ParentEndpoint`. Use inheritance to share behavior across endpoints, such as [auth requirements](../authentication/basics), to extend or reshape endpoints from modules, and to control what the generated client exposes. -Currently, there are the following possibilities to extend another `Endpoint` class: +How the parent is marked decides what gets generated for it. An `abstract` parent is not exposed on the server, and a `@doNotGenerate` parent gets no client code. Your concrete subclass always gets a client, unless it opts out itself. The sections below walk through each case. ## Inheriting from an `Endpoint` class @@ -194,13 +193,13 @@ class MyCalculatorEndpoint extends CalculatorEndpoint { } ``` -Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. `MyCalculatorEndpoint` will expose both the `add` and `addBig` methods, since `addBig` was overridden and thus lost the `@doNotGenerate` annotation. +Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. The generated `MyCalculatorEndpoint` client will expose both the `add` and `addBig` methods, since annotations are not inherited: overriding `addBig` without repeating `@doNotGenerate` re-exposes it. ## Building base endpoints for behavior -Endpoint subclassing is not just useful to inherit (or hide) methods, it can also be used to pre-configure any other property of the `Endpoint` class. +Beyond inheriting or hiding methods, endpoint subclassing can pre-configure any other property of the `Endpoint` class. -For example, you could define a base class that requires callers to be logged in: +For example, you could define a base class that requires callers to be signed in, using the `requireLogin` flag from [authentication on endpoints](../authentication/basics): ```dart abstract class LoggedInEndpoint extends Endpoint { @@ -211,7 +210,7 @@ abstract class LoggedInEndpoint extends Endpoint { And now every endpoint that extends `LoggedInEndpoint` will check that the user is logged in. -Similarly, you could wrap up a specific set of required scopes in a base endpoint, which you can then easily use for the app's endpoints instead of repeating the scopes in each: +Similarly, you could wrap up a specific set of required scopes in a base endpoint, and use it for the app's endpoints instead of repeating the scopes in each: ```dart abstract class AdminEndpoint extends Endpoint { @@ -220,7 +219,7 @@ abstract class AdminEndpoint extends Endpoint { } ``` -Again, just have your custom endpoint extend `AdminEndpoint` and you can be sure that the user has the appropriate permissions. +Have your custom endpoint extend `AdminEndpoint`, and the user is guaranteed to hold the appropriate [scopes](../authentication/basics). ## Client-side endpoint inheritance @@ -328,7 +327,7 @@ var advancedCalc = client.getEndpointOfType('advancedCalcula This pattern is especially useful for modules. A module can provide an abstract endpoint that defines an interface, and users of the module can extend it to expose the functionality on their server: -**In a module (e.g., `serverpod_auth`):** +**In a module:** Declare an abstract endpoint with common methods on the server: @@ -378,7 +377,7 @@ class UserLoggedInWidget extends StatelessWidget { **In the user application:** -The user will just have to extend the abstract endpoint to expose it on their server. Then, any client code that depends on the abstract endpoint will work regardless of the concrete class name or location. +The user extends the abstract endpoint to expose it on their server. Then, any client code that depends on the abstract endpoint will work regardless of the concrete class name or location. ```dart // Extend the module's abstract endpoint to expose it diff --git a/docs/06-concepts/02-endpoints-and-apis/07-streaming.md b/docs/06-concepts/02-endpoints-and-apis/07-streaming.md deleted file mode 100644 index 27d700e2..00000000 --- a/docs/06-concepts/02-endpoints-and-apis/07-streaming.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -description: Stream data between server and client in Serverpod using streaming methods over a self-managed WebSocket connection, with support for bidirectional streams and serializable exceptions. ---- - -# Streams - -For some applications, it's not enough to be able to call server-side methods. You may also want to push data from the server to the client or send data two-way. Examples include real-time games or chat applications. Luckily, Serverpod supports a framework for streaming data. It's possible to stream any serialized objects to or from any endpoint. - -Serverpod supports streaming data through [streaming methods](#streaming-methods), which imitate how `Streams` work in Dart and offer a simple interface that automatically handles the connection with the server. - -:::tip - -For a real-world example, check out [Pixorama](https://pixorama.live). It's a multi-user drawing experience showcasing Serverpod's real-time capabilities and comes with complete source code. - -::: - -## Streaming Methods - -When an endpoint method is defined with `Stream` instead of `Future` as the return type or includes `Stream` as a method parameter, it is recognized as a streaming method. Streaming methods transmit data over a shared, self-managed web socket connection that automatically connects and disconnects from the server. - -### Defining a streaming method - -Streaming methods are defined by using the `Stream` type as either the return value or a parameter. - -Following is an example of a streaming method that echoes back any message: - -```dart -class ExampleEndpoint extends Endpoint { - Stream echoStream(Session session, Stream stream) async* { - await for (var message in stream) { - yield message; - } - } -} -``` - -The generic for the `Stream` can also be defined, e.g., `Stream`. This definition is then included in the client, enabling static type validation. - -The streaming method above can then be called from the client like this: - -```dart -var inStream = StreamController(); -var outStream = client.example.echoStream(inStream.stream); -outStream.listen((message) { - print('Received message: $message'); -}); - -inStream.add('Hello'); -inStream.add(42); - -// This will print -// Received message: Hello -// Received message: 42 -``` - -In the example above, the `echoStream` method passes back any message sent through the `outStream`. - -:::tip - -The stream is defined as dynamic and can contain any type that can be serialized by Serverpod. - -::: - -### Lifecycle of a streaming method - -Each time the client calls a streaming method, a new `Session` is created, and a call with that `Session` is made to the method endpoint on the server. The `Session` is automatically closed when the streaming method call is over. - -If the web socket connection is lost, all streaming methods are closed on the server and the client. - -When the streaming method is defined with a returning `Stream`, the method is kept alive until the stream subscription is canceled on the client or the method returns. - -When the streaming method returns a `Future`, the method is kept alive until the method returns. - -Streams in parameters are closed when the stream is closed. This can be done by either closing the stream on the client or canceling the subscription on the server. - -All streams in parameters are closed when the method call is over. - -### Authentication - -Authentication is integrated into streaming method calls. When a client initiates a streaming method, the server automatically authenticates the session. - -Authentication is validated when the stream is first established, using the authentication data stored in the `Session` object. If a user's authentication is revoked, the stream is closed and an exception is thrown. - -For more details on handling revoked authentication, refer to the section on [handling revoked authentication](../authentication/custom-overrides#handling-revoked-authentication). - -### WebSocket ping interval - -The server sends periodic ping messages on open streaming connections to keep them alive. The interval between pings is configurable and defaults to 30 seconds. - -If you deploy behind a load balancer or proxy with a shorter idle timeout (for example, 15-20 seconds), you may need to lower the ping interval so connections are not closed. Set the `SERVERPOD_WEBSOCKET_PING_INTERVAL` environment variable to the desired interval in seconds, or configure `websocketPingInterval` in your config file; see the [Configuration reference](../lookups/configuration-reference). - -### Error handling - -Error handling works just like in regular endpoint methods in Serverpod. If an exception is thrown on a stream, the stream is closed with an exception. If the exception thrown is a serializable exception, the exception is first serialized and passed over the stream before it is closed. - -This is supported in both directions; stream parameters can pass exceptions to the server, and return streams can pass exceptions to the client. - -```dart -class ExampleEndpoint extends Endpoint { - Stream echoStream(Session session, Stream stream) async* { - stream.listen((message) { - // Do nothing - }, onError: (error) { - print('Server received error: $error'); - throw SerializableException('Error from server'); - }); - } -} -``` - -```dart -var inStream = StreamController(); -var outStream = client.example.echoStream(inStream.stream); -outStream.listen((message) { - // Do nothing -}, onError: (error) { - print('Client received error: $error'); -}); - -inStream.addError(SerializableException('Error from client')); - -// This will print -// Server received error: Error from client -// Client received error: Error from server -``` - -In the example above, the client sends an error to the server, which then throws an exception back to the client. And since the exception is serializable, it is passed over the stream before the stream is closed. - -Read more about serializable exceptions here: [Serializable exceptions](./error-handling-and-exceptions). - -## Example: live updates for a filtered query - -A common real-time need is to push updates for one record or filter, for example the messages in a single chat room. Combine a streaming method with [server events](./server-events): the streaming method subscribes the client to a channel scoped to that filter, and whatever changes the data posts to the same channel. - -```dart -class ChatEndpoint extends Endpoint { - // The client subscribes to live messages for one room. - Stream watchRoom(Session session, int roomId) async* { - yield* session.messages.createStream('room_$roomId'); - } - - // Posting a message stores it and broadcasts it to everyone watching the room. - Future postToRoom(Session session, int roomId, String text) async { - var message = ChatMessage(roomId: roomId, text: text); - await ChatMessage.db.insertRow(session, message); - await session.messages.postMessage('room_$roomId', message); - } -} -``` - -On the client, listen to the returned stream and call `postToRoom` to publish: - -```dart -var messages = client.chat.watchRoom(roomId); -messages.listen((message) { - print('Room ${message.roomId}: ${message.text}'); -}); - -await client.chat.postToRoom(roomId, 'Hello, room!'); -``` - -Because the channel name includes the `roomId`, each client receives updates only for the room it is watching. The same pattern works for any filter: scope the channel by the id or query you care about, and post to it whenever the data changes. To fan the updates out across multiple server instances, post with `global: true` (see [Global messages](./server-events#global-messages)). - -## Streaming endpoints (deprecated) - -Serverpod's original streaming API (`streamOpened`, `handleStreamMessage`, `sendStreamMessage`, `openStreamingConnection`) is deprecated and will be removed in a future version. Use [streaming methods](#streaming-methods) instead. The old API is kept for reference in [Streaming endpoints (deprecated)](../../upgrading/archive/streaming-endpoints). diff --git a/docs/06-concepts/02-endpoints-and-apis/08-endpoint-middleware.md b/docs/06-concepts/02-endpoints-and-apis/08-endpoint-middleware.md new file mode 100644 index 00000000..42b672b0 --- /dev/null +++ b/docs/06-concepts/02-endpoints-and-apis/08-endpoint-middleware.md @@ -0,0 +1,141 @@ +--- +sidebar_label: Endpoint middleware +description: Middleware on the API server intercepts endpoint requests and responses for concerns such as logging, caching, and rate limiting. +--- + +# Endpoint middleware + +Middleware wraps every request to the API server, before and after your endpoints, making it suitable for logging, caching, and rate limiting. Serverpod middleware follows the middleware interface of [Relic](https://docs.dartrelic.dev/reference/middleware), the HTTP server framework Serverpod is built on. To add middleware to web server routes instead, scoped to path prefixes, see [Web server middleware](../web-server/web-server-middleware). + +## Add middleware to your server + +Add middleware to your server in the `run` function before starting the server: + +```dart +import 'package:serverpod/serverpod.dart'; + +void run(List args) async { + final pod = Serverpod( + args, + Protocol(), + Endpoints(), + ); + + // Add middleware before starting + pod.server.addMiddleware(myCustomMiddleware()); + + await pod.start(); +} +``` + +Middleware runs on every API-server request in the order it is added: not only endpoint method calls, but also the health check, WebSocket upgrades for [streaming](./streaming), and file-storage requests. Keep each middleware focused and efficient, and return early for paths it does not care about. + +## Create custom middleware + +### Middleware signature + +A middleware is a function with this signature: + +```dart +typedef Handler = FutureOr Function(Request request); +typedef Middleware = Handler Function(Handler innerHandler); +``` + +The return value is a `Result`, which can be a `Response`, `Hijack`, or `WebSocketUpgrade`. Check `is Response` before modifying response-specific fields. The types come with the `package:serverpod/serverpod.dart` import. No separate Relic import is needed. + +### Add a header to all responses + +Here's a basic middleware that adds a custom header to all responses: + +```dart +Middleware customHeaderMiddleware() { + return (Handler innerHandler) { + return (Request req) async { + // Inspect or modify `req` here if needed + + // Call the inner handler to process the request + final result = await innerHandler(req); + + // Modify the response + if (result is Response) { + return result.copyWith( + headers: result.headers.transform((h) { + h['X-Custom-Header'] = ['my-value']; + }), + ); + } + + return result; + }; + }; +} +``` + +### Rate limit requests + +Middleware can answer a request itself instead of calling the inner handler, which is how a rate limiter rejects excess traffic. This example allows each client IP a fixed number of requests per time window: + +```dart +import 'package:serverpod/serverpod.dart'; + +/// Limits each client IP to [maxRequests] requests per [window]. +/// +/// Requests over the limit receive a 429 Too Many Requests response. +Middleware rateLimitMiddleware({ + int maxRequests = 60, + Duration window = const Duration(minutes: 1), +}) { + final clients = {}; + + return (Handler innerHandler) { + return (Request req) async { + final ip = req.connectionInfo.remote.address.toString(); + final now = DateTime.now(); + + var client = clients[ip]; + if (client == null || now.difference(client.windowStart) >= window) { + // Start a new window for this client. + client = (windowStart: now, count: 0); + } + client = (windowStart: client.windowStart, count: client.count + 1); + clients[ip] = client; + + if (client.count > maxRequests) { + return Response( + 429, + body: Body.fromString('Too many requests. Try again later.'), + ); + } + + return innerHandler(req); + }; + }; +} +``` + +When adapting it, keep three things in mind: + +- Behind a load balancer or proxy, `req.connectionInfo` is the nearest network hop. Read the client address from a forwarding header such as `X-Forwarded-For` instead. +- Middleware wraps all API-server requests, so the counter also ticks for health checks and other non-endpoint traffic. +- The in-memory map grows with each new client IP and is local to one server. Production quota patterns need eviction, and state shared across servers such as a database counter. + +## Handle errors in middleware + +Middleware can catch errors from the handlers it wraps, for example to record metrics, before rethrowing. Serverpod's own error handling wraps all user middleware, so a rethrown exception still gets the standard status mapping. There is no `Session` in middleware, so use your own logging or metrics facility here. + +```dart +Middleware errorHandlingMiddleware() { + return (Handler innerHandler) { + return (Request req) async { + try { + return await innerHandler(req); + } catch (e) { + // Record metrics or logs here. + + // Re-throw to let Serverpod handle it + rethrow; + } + }; + }; +} +``` diff --git a/docs/06-concepts/02-endpoints-and-apis/08-server-events.md b/docs/06-concepts/02-endpoints-and-apis/08-server-events.md deleted file mode 100644 index 9868fc4a..00000000 --- a/docs/06-concepts/02-endpoints-and-apis/08-server-events.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -description: Use Serverpod's built-in event messaging system to send and receive messages within or across servers, coordinating streams and shared state via channels. ---- - -# Server events - -Serverpod framework comes with a built-in event messaging system. This enables efficient message exchange within and across servers, making it ideal for scenarios where shared state is needed, such as coordinating streams or managing data across a server cluster. - -The event message system is accessed on the `Session` object through the field `messages`. - -## Quick Reference - -Here is a quick reference to the key messaging methods: - -| Method | Description | -| ---------------------- | ------------------------------------------ | -| `postMessage` | Send a message to a channel. | -| `addListener` | Add a listener to a channel. | -| `removeListener` | Remove a listener from a channel. | -| `createStream` | Create a stream that listens to a channel. | -| `revokeAuthentication` | Revoke authentication tokens. | - -## Sending messages - -To send a message, you can use the `postMessage` method. The message is published to the specified channel and needs to be a Serverpod model. - -```dart -var message = UserUpdate(); // Model that represents changes to user data. -session.messages.postMessage('user_updates', message); -``` - -In the example above, the message is published on the `user_updates` channel. Any subscriber to this channel in the server will receive the message. - -### Global messages - -Serverpod uses Redis to pass messages between servers. To send a message to another server, enable Redis and then set the `global` parameter to `true` when posting a message. - -```dart -var message = UserUpdate(); // Model that represents changes to user data. -session.messages.postMessage('user_updates', message, global: true); -``` - -In the example above, the message is published to the `user_updates` channel and will be received by all servers connected to the same Redis instance. - -:::warning - -If Redis is not enabled, sending global messages will throw an exception. - -::: - -## Receiving messages - -Serverpod provides two ways to handle incoming messages: by creating a stream that subscribes to a channel or by adding a listener to a channel. - -### Creating a stream - -To create a stream that subscribes to a channel, use the `createStream` method. The stream will emit a value whenever a message is posted to the channel. - -```dart -var stream = session.messages.createStream('user_updates'); -stream.listen((message) { - print('Received message: $message'); -}) -``` - -In the above example, a stream is created that listens to the `user_updates` channel and processes incoming messages. - -#### Stream lifecycle - -The stream is automatically closed when the session is closed. To close the stream manually, cancel the stream subscription. - -```dart -var stream = session.messages.createStream('user_updates'); -var subscription = stream.listen((message) { - print('Received message: $message'); -}); - -subscription.cancel(); -``` - -### Adding a listener - -To add a listener to a channel, use the `addListener` method. The listener will be called whenever a message is posted to the channel. - -```dart -session.messages.addListener('user_updates', (message) { - print('Received message: $message'); -}); -``` - -In the above example, the listener will be called whenever a message is posted to the `user_updates` channel. The listener will be called regardless of whether a message is published globally by another server or internally by the same server. - -#### Listener lifecycle - -The listener is automatically removed when the session is closed. To manually remove a listener, use the `removeListener` method. - -```dart -var myListenerCallback = (message) { - print('Received message: $message'); -}; -// Register the listener -session.messages.addListener('user_updates', myListenerCallback); - -// Remove the listener -session.messages.removeListener('user_updates', myListenerCallback); -``` - -In the above example, the listener is first added and then removed from the `user_updates` channel. - -## Revoke authentication - -The messaging interface also exposes a method for revoking authentication. For more details on handling revoked authentication, refer to the section on [handling revoked authentication](../authentication/custom-overrides#handling-revoked-authentication). diff --git a/docs/06-concepts/02-endpoints-and-apis/04-configure-http-calls.md b/docs/06-concepts/02-endpoints-and-apis/09-configure-http-calls.md similarity index 86% rename from docs/06-concepts/02-endpoints-and-apis/04-configure-http-calls.md rename to docs/06-concepts/02-endpoints-and-apis/09-configure-http-calls.md index fd3d1e55..0257b80f 100644 --- a/docs/06-concepts/02-endpoints-and-apis/04-configure-http-calls.md +++ b/docs/06-concepts/02-endpoints-and-apis/09-configure-http-calls.md @@ -1,10 +1,10 @@ --- -description: Configure HTTP calls to set CORS credentials for web apps or use platform-native networking libraries. +description: HTTP transport for the generated client is configurable through httpClientOverride, for CORS credentials on web and native network stacks. --- # Configure HTTP calls -The generated `Client` accepts an optional `httpClientOverride` parameter that controls the underlying HTTP transport used for API calls. Use it when you need to customize how requests are sent, such as enabling browser credentials or using platform-native HTTP stacks. +Two situations call for customizing how your app sends requests: a web app that must include cookies on cross-origin calls, and apps that want the platform-native network stacks on iOS and Android. Both are handled by the `httpClientOverride` parameter on the generated `Client`, which swaps the underlying HTTP transport. The `serverUrl` in the examples below is the resolved server address from [calling endpoints](../endpoints-and-apis#call-an-endpoint-from-your-app). ## Include CORS credentials on web @@ -19,6 +19,8 @@ final client = Client( ); ``` +### Allow credentials on the server + On the server, Serverpod adds CORS headers to API responses by default through `httpResponseHeaders` and `httpOptionsResponseHeaders` on the `Serverpod` constructor. The defaults allow cross-origin `POST` requests from any origin (`Access-Control-Allow-Origin: *`) and permit common request headers such as `Authorization` on preflight `OPTIONS` requests. Credential-aware requests require `Access-Control-Allow-Credentials: true` and a specific origin instead of the wildcard. Override the defaults in your `lib/server.dart` (or wherever you construct `Serverpod`): @@ -85,12 +87,12 @@ void main() async { final engine = CronetEngine.build( cacheMode: CacheMode.memory, cacheMaxSize: 2 * 1024 * 1024, - userAgent: 'Book Agent'); + userAgent: 'my-app'); httpClient = CronetClient.fromCronetEngine(engine, closeEngine: true); } else if (Platform.isIOS || Platform.isMacOS) { final config = URLSessionConfiguration.ephemeralSessionConfiguration() ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) - ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + ..httpAdditionalHeaders = {'User-Agent': 'my-app'}; httpClient = CupertinoClient.fromSessionConfiguration(config); } diff --git a/docs/06-concepts/04-authentication/05-token-managers/02-jwt-token-manager.md b/docs/06-concepts/04-authentication/05-token-managers/02-jwt-token-manager.md index a21af3db..ac198415 100644 --- a/docs/06-concepts/04-authentication/05-token-managers/02-jwt-token-manager.md +++ b/docs/06-concepts/04-authentication/05-token-managers/02-jwt-token-manager.md @@ -220,5 +220,5 @@ await TokenMetadata.db.insertRow( When using the `JwtTokenManager` in the server, no extra configuration is needed on the client. It will automatically include the access token in requests to the server and eagerly refresh the token when it is 30 seconds away from expiring. In case the refresh token expires, the client will automatically sign the user out and redirect to the login page. :::warning -The deprecated `client.openStreamingConnection()` interface is not compatible with JWT authentication. If you are using JWT tokens, migrate to [streaming endpoints](../../endpoints-and-apis/streaming) instead. +The deprecated `client.openStreamingConnection()` interface is not compatible with JWT authentication. If you are using JWT tokens, migrate to [streaming methods](../../endpoints-and-apis/streaming) instead. ::: diff --git a/docs/06-concepts/cli/commands/generate/_generate.md b/docs/06-concepts/cli/commands/generate/_generate.md index 6e8be126..b46e3d8b 100644 --- a/docs/06-concepts/cli/commands/generate/_generate.md +++ b/docs/06-concepts/cli/commands/generate/_generate.md @@ -2,4 +2,4 @@ `serverpod generate` reads your model and endpoint definitions and generates the server and client Dart code that connects them. Run it whenever you add or change a model or an endpoint. -Pass `--watch` to regenerate automatically as you edit, or `--force` to rebuild after hand-editing generated files. For what gets generated, see [Working with endpoints](/concepts/working-with-endpoints) and [Models](/concepts/models). +Pass `--watch` to regenerate automatically as you edit, or `--force` to rebuild after hand-editing generated files. For what gets generated, see [Working with endpoints](../../endpoints-and-apis) and [Models](../../data-and-the-database/models).