diff --git a/src/pages/docs/auth/identified-clients.mdx b/src/pages/docs/auth/identified-clients.mdx index e175177c06..e4754fa566 100644 --- a/src/pages/docs/auth/identified-clients.mdx +++ b/src/pages/docs/auth/identified-clients.mdx @@ -9,6 +9,19 @@ When a client is assigned a trusted identity, that is, a `clientId`, then they a For example, assume you are building a chat application and want to allow clients to publish messages and be present on a channel. If each client is assigned a trusted identity by your server, such as a unique email address or UUID, then all other subscribed clients can trust any messages or presence events they receive in the channel as being from that client. No other clients are permitted to assume a `clientId` that they are not assigned in their Ably-compatible token. They are unable to masquerade as another `clientId`. +## ClientId immutability + +A connection's `clientId`, once set, is immutable. You cannot change a `clientId` after it has been established for a connection. + +### Late initialization + +It is possible to have late initialization of `clientId` in certain scenarios: + +* AuthCallback discovery: When using `authCallback`, the `clientId` may only become known when the first authCallback completes and returns a token with a `clientId`. +* Opaque token processing: When the library receives an opaque token string, it only learns the `clientId` when it receives a response from the server that has processed the token. + +In both cases, the immutability requirement is satisfied because the `clientId` gets set as part of the client's first interaction with the server. Once this initial authentication is complete, the `clientId` cannot be changed for that connection. + ## Assign a clientId There are three different ways a client can be identified with using a `clientId`: diff --git a/src/pages/docs/auth/token.mdx b/src/pages/docs/auth/token.mdx index e2927166f7..cf3da84cc1 100644 --- a/src/pages/docs/auth/token.mdx +++ b/src/pages/docs/auth/token.mdx @@ -23,8 +23,35 @@ One of the important benefits of using an Ably SDK is that the automatic refresh To use automatic refresh of tokens, provide either an `authUrl` or an `authCallback`. When the token is near to expiry the `authUrl` or `authCallback` is invoked and a new token is automatically requested. +### Automatic renewal timing + +The Ably SDK will automatically renew the authentication token when it is within 30 seconds of expiry to ensure no loss of service. This automatic renewal process ensures that connections remain active without interruption due to token expiration. + +### Token authentication workflow + +Understanding how token authentication works helps clarify why automatic renewal is essential: + +1. Your server uses the Ably API key to request a 'Token Request' object from Ably. +2. Your client uses this 'Token Request' object to request the actual token from the Ably server every time it needs to authenticate. +3. These tokens are short-lived and expire after a certain period of time. +4. The client SDK automatically requests a new token just before the previous one expires, ensuring the connection never drops due to authentication failure. + +Using an `authUrl` or `authCallback` ensures your client automatically requests a new token when needed, making the authentication process seamless and preventing service interruptions. + An `authURL` is recommended for use with web-based clients as they can easily utilize cookies and other web-only features. For non-web clients, `authCallback` is the recommended strategy. +### Token TTL limits + +Ably enforces maximum TTL (time-to-live) limits on different types of tokens: + +- Access tokens: Maximum TTL of 24 hours. +- Device tokens (for push notifications): Maximum TTL of 5 years. +- Revocable tokens: Maximum TTL of 1 hour (when [token revocation](/docs/auth/revocation#revocable-tokens) is enabled). + + + ### authUrl You can specify an `authUrl` when you create the Ably client. For example: @@ -1004,7 +1031,7 @@ signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) jwt := base64Header + "." + base64Claims + "." + signature log.Println("JWT:", jwt) -// Send JWT to client (for demonstration, we print it here) +// Send JWT to client (for demonstration, print it here) ``` ```flutter @@ -1041,6 +1068,47 @@ final ablyJwt = "$base64Header.$base64Claims.$signature"; ``` +## Dynamic channel access control + +Token authentication allows you to dynamically change a client's channel access permissions without disconnecting. Use the [`authorize()`](/docs/api/realtime-sdk/authentication#authorize) method to re-authenticate with updated [capabilities](/docs/auth/capabilities). + +When you call `client.auth.authorize()`: + +1. The client obtains a new token with different capabilities from your authentication server. +2. The new token is sent to Ably using the existing realtime connection. +3. The updated permissions are automatically applied without disconnecting. + +### Use cases + +You can trigger re-authentication in response to: +- Detect error code [40160](/docs/platform/errors/codes#40160) when a client attempts to attach to a channel. +- Instruct clients to re-authenticate via Ably channels or other communication methods. + +The following example shows how to re-authenticate with updated capabilities: + + +```javascript +// Re-authenticate to get new capabilities +try { + const newTokenDetails = await client.auth.authorize({ + clientId: 'user123', + // Your auth server will return a token with updated capabilities + }); + console.log('Successfully updated permissions'); +} catch (error) { + console.error('Failed to re-authenticate:', error); +} +``` + + +For security purposes, handle non-compliant clients by: +- Set shorter token TTL expiry times to force frequent re-authentication (minimum 10 minutes recommended). +- Use the [token revocation API](/docs/auth/revocation) to immediately invalidate tokens. + + + ## When to use token auth Ably recommends that token authentication is used client-side for the following reasons: diff --git a/src/pages/docs/channels/index.mdx b/src/pages/docs/channels/index.mdx index d326c89279..acd6a715b4 100644 --- a/src/pages/docs/channels/index.mdx +++ b/src/pages/docs/channels/index.mdx @@ -43,7 +43,7 @@ Channels are identified by their unique name. The following restrictions apply t * They can't be empty * They can't contain newline characters -While Ably doesn't limit the length of channel names, we recommend you keep them under 2048 characters, since some older browsers have trouble with long URLs. +While Ably doesn't limit the length of channel names, keeping them under 2048 characters is recommended, since some older browsers have trouble with long URLs. Use the [`get()`](/docs/api/realtime-sdk/channels#get) method to create or retrieve a channel instance: @@ -191,7 +191,7 @@ The channel rules related to security and client identity are: | Rule | Description | |------|-------------| | Identified | If enabled, clients will not be permitted to use (including to attach, publish, or subscribe) matching channels unless they are [identified](/docs/auth/identified-clients) (they have an assigned client ID). Anonymous clients are not permitted to join these channels. Find out more about [authenticated and identified clients](/docs/auth/identified-clients). | -| TLS only | If enabled, only clients who have connected to Ably over TLS will be allowed to use matching channels. By default all of Ably's client libraries use TLS when communicating with Ably over REST or when using our Realtime transports such as Websockets. | +| TLS only | If enabled, only clients who have connected to Ably over TLS will be allowed to use matching channels. By default all of Ably's client libraries use TLS when communicating with Ably over REST or when using Realtime transports such as Websockets. | The channel rules related to enabling features are: diff --git a/src/pages/docs/connect/index.mdx b/src/pages/docs/connect/index.mdx index f79cf245c4..90fe8b3416 100644 --- a/src/pages/docs/connect/index.mdx +++ b/src/pages/docs/connect/index.mdx @@ -137,7 +137,7 @@ It isn't possible to share a connection between browser tabs. This is because br A connection ID is a unique identifier given to a connection, allowing for identifying and specifying particular connections. -An active connection ID is guaranteed to be unique in the Ably system whilst it is active, i.e. no other connection will share that connection ID. However, Ably reserves the right to generate a new connection ID later that may be the same as a previously discarded connection ID (once the connection is closed). Therefore we advise customers to not use the connection ID as a perpetual unique identifier as it is possible that a connection ID may be used many times. +An active connection ID is guaranteed to be unique in the Ably system whilst it is active, i.e. no other connection will share that connection ID. However, Ably reserves the right to generate a new connection ID later that may be the same as a previously discarded connection ID (once the connection is closed). Therefore customers are advised to not use the connection ID as a perpetual unique identifier as it is possible that a connection ID may be used many times. ### Connection metachannels @@ -200,6 +200,61 @@ AblyRealtime ably = new AblyRealtime(options); ``` + +## Browser page unload behavior +In browser environments, ably-js automatically handles page unload events to ensure connections are properly closed when users navigate away or close pages. +### Default `beforeunload` behavior +By default, the Ably Pub/Sub JavaScript SDK adds a listener for the `beforeunload` event to cleanly close connections before a page is closed. This provides orderly behavior where: +* Connections are seen as having closed immediately by Ably servers. +* Presence members associated with the connection are seen by all users as having left immediately. + +If a connection to Ably is not explicitly closed when there is a page unload event, then the connection state is preserved by Ably for 2 minutes. Preserving connection state for 2 minutes when there is an unexpectedly dropped connection provides the opportunity for the client to reconnect and resume the connection without losing any messages. + +### Reliability considerations +The `beforeunload` event can be unreliable and is not guaranteed to fire under certain circumstances: +* The event may fire but the page is subsequently not disposed of (navigation can be cancelled). +* The handler in ably-js that closes a connection on a `beforeunload` event is hazardous unless the application developer is certain that there is no case where `beforeunload` fires, but the page is subsequently not unloaded. +* Recent releases of Chrome (version 108+) have introduced a Memory Saver feature that can cause pages to be discarded without firing `beforeunload` events. + +### Chrome Memory Saver impact +Chrome's Memory Saver feature assists with controlling the browser's memory footprint by discarding inactive tabs. This significantly increases the frequency of pages being discarded, which: +* Causes JavaScript execution to stop without opportunity to intercept the event. +* Prevents connections from closing immediately since `beforeunload` doesn't fire. +* Results in longer delays before Ably recognizes the connection has closed. +* Affects presence members leaving immediately. + +### Managing connection lifecycle +To ensure predictable connection closure behavior, consider these options: + +Set `closeOnUnload:false` in [`ClientOptions`](/docs/api/realtime-sdk#client-options) when initializing the library: + +```realtime_javascript +const ably = new Ably.Realtime({ + key: 'your-api-key', + closeOnUnload: false +}); +``` +```realtime_nodejs +const ably = new Ably.Realtime({ + key: 'your-api-key', + closeOnUnload: false +}); +``` + +Manage connection lifecycle explicitly by calling [`close()`](/docs/api/realtime-sdk/connection#close) on the Ably realtime instance when it's no longer needed: + +```realtime_javascript +// When your application determines the connection should close +ably.close(); +``` +```realtime_nodejs +// When your application determines the connection should close +ably.close(); +``` + +Disable Chrome Memory Saver globally or on a site-by-site basis in browser settings if the feature is impacting your application's behavior. + + ## Close a connection A connection to Ably should be closed once it is no longer needed. Note that there is a 2 minute delay before a connection is closed, if the [`close()`](/docs/api/realtime-sdk/connection#close) method hasn't been explicitly called. This is important to consider in relation to the number of [concurrent connections](/docs/platform/pricing/limits#connection) to your account. diff --git a/src/pages/docs/connect/states.mdx b/src/pages/docs/connect/states.mdx index 6b929c0a76..5755fbfde3 100644 --- a/src/pages/docs/connect/states.mdx +++ b/src/pages/docs/connect/states.mdx @@ -15,6 +15,18 @@ An Ably SDK is responsible for managing a connection. This includes: When an SDK is instantiated it will establish a connection immediately, and if the connection drops at any time it will attempt to re-establish it by making repeated connection attempts every 15 seconds for up to two minutes. +### System-initiated disconnections + +The Ably system can validly drop client connections at any point for operational reasons, including: + +* Internal autoscaling - Automatic scaling of Ably's infrastructure +* Connection rebalancing - Distributing connections across servers for optimal performance +* Service deployments - Updates to services that clients are connected to or routed through + +When this occurs, the Ably SDK will immediately and automatically reconnect, using connection state recovery functionality to resume with full connection continuity (no missed messages). This process is typically exposed as a connection state change from `connected` to `connecting` (usually with error code 80003), followed by a return to `connected` once reconnected. The complete sequence generally takes just a few hundred milliseconds. + +This behavior is normal and expected - these brief disconnections are not indicative of problems with your application or network connectivity. + ## Available connection states The different connection states are: @@ -24,11 +36,11 @@ The different connection states are: | `initialized` | A `Connection` object has been initialized but not yet connected. | | `connecting` | A connection attempt has been initiated, this state is entered as soon as an SDK has completed initialization, and is re-entered each time connection is re-attempted following disconnection. | | `connected` | A connection exists and is active. | -| `disconnected` | A temporary failure condition when no current connection exists. The disconnected state is entered if an established connection is dropped, or if a connection attempt is unsuccessful. In the disconnected state an SDK will periodically attempt to open a new connection (approximately every 15 seconds), anticipating the connection will be re-established soon and connection and channel continuity will be possible. In this state, developers can continue to publish messages as they are automatically placed in a local queue, sent when connection is re-established. Messages published by other clients whilst this client is disconnected will be delivered to it when reconnected if the connection was resumed within two minutes. After two minutes have elapsed, recovery is no longer possible and the connection will move to the `suspended` state. | -| `suspended` | A long term failure condition when no current connection exists because there is no network connectivity or available host. The suspended state is entered after a failed connection attempt if there has then been no connection for a period of two minutes. In the suspended state, an SDK will periodically attempt to open a new connection every 30 seconds. Developers are unable to publish messages in this state. A new connection attempt can also be triggered by an explicit call to [`connect()`](/docs/api/realtime-sdk/connection#connect) on the `Connection` object. Once the connection has been re-established, channels will be automatically re-attached. The client has been disconnected for too long for them to resume from where they left off, so if it wants to catch up on messages published by other clients while it was disconnected, it needs to use the [history API](/docs/storage-history/history). | +| `disconnected` | A temporary failure condition when no current connection exists. The disconnected state is entered if an established connection is dropped, or if a connection attempt is unsuccessful. In the disconnected state an SDK will periodically attempt to open a new connection (approximately every 15 seconds), anticipating the connection will be re-established soon and connection and channel continuity will be possible. In this state, developers can continue to publish messages as they are automatically placed in a local queue, sent when connection is re-established. Messages published by other clients whilst this client is disconnected will be delivered to it when reconnected if the connection was resumed within two minutes. After two minutes have elapsed, recovery is no longer possible and the connection will move to the `suspended` state. Publishing: Attempting to publish while disconnected will return error code [80003](/docs/platform/errors/codes#80003). | +| `suspended` | A long term failure condition when no current connection exists because there is no network connectivity or available host. The suspended state is entered after a failed connection attempt if there has then been no connection for a period of two minutes. In the suspended state, an SDK will periodically attempt to open a new connection every 30 seconds. Developers are unable to publish messages in this state. A new connection attempt can also be triggered by an explicit call to [`connect()`](/docs/api/realtime-sdk/connection#connect) on the `Connection` object. Once the connection has been re-established, channels will be automatically re-attached. The client has been disconnected for too long for them to resume from where they left off, so if it wants to catch up on messages published by other clients while it was disconnected, it needs to use the [history API](/docs/storage-history/history). Publishing: Attempting to publish while suspended will return error code [80002](/docs/platform/errors/codes#80002). | | `closing` | An explicit request by the developer to close the connection has been sent to the Ably service. If a reply is not received from Ably shortly, the connection will be forcibly terminated and the connection state will become `closed`. | | `closed` | The connection has been explicitly closed by the client. In the closed state, no reconnection attempts are made automatically by an SDK, and clients may not publish messages. No connection state is preserved by the service or by an SDK. A new connection attempt can be triggered by an explicit call to [`connect()`](/docs/api/realtime-sdk/connection#connect) on the `Connection` object, which will result in a new connection. | -| `failed` | This state is entered if an SDK encounters a failure condition that it cannot recover from. This may be a fatal connection error received from the Ably service (e.g. an attempt to connect with an incorrect API key), or some local terminal error (e.g. the token in use has expired and the SDK does not have any way to renew it). In the failed state, no reconnection attempts are made automatically by an SDK, and clients may not publish messages. A new connection attempt can be triggered by an explicit call to [`connect()`](/docs/api/realtime-sdk/connection#connect) on the `Connection` object. | +| `failed` | This state is entered if an SDK encounters a failure condition that it cannot recover from. This may be a fatal connection error received from the Ably service (e.g. an attempt to connect with an incorrect API key), or some local terminal error (e.g. the token in use has expired and the SDK does not have any way to renew it). In the failed state, no reconnection attempts are made automatically by an SDK, and clients may not publish messages. A new connection attempt can be triggered by an explicit call to [`connect()`](/docs/api/realtime-sdk/connection#connect) on the `Connection` object. Publishing: Attempting to publish while failed will return error code [80000](/docs/platform/errors/codes#80000). | ### Example connection state sequences @@ -392,6 +404,50 @@ Recover the previous connection state conditionally based on some logic: Please note that as [`sessionStorage`](https://www.w3.org/TR/webstorage/) is used to persist the `LastConnectionDetails` between page reloads, it is only available for pages in the same origin and top-level browsing context. +#### Multiple SDK instances on the same origin + +When using the default recovery behavior with multiple ably-js SDK instances on the same origin (in different iframes or different contexts within the same page), sessionStorage key collisions can occur since the recovery key is stored under a fixed sessionStorage key name. + +To handle multiple SDK instances that need independent recovery, you should manually manage the recovery keys with context-specific sessionStorage keys: + + +```realtime_javascript +const clientOpts = { + closeOnUnload: false, // Prevent connection being closed server-side when page closes + key: 'your-api-key' + // ... other auth options +}; + +// Use a unique sessionStorage key for each SDK context +// This prevents recovery key collisions between multiple instances +const sessionStorageKey = 'ably-sdk-recoverykey-for-mainpage'; // Make this specific to your context + +// Recover from previous session if one exists +const previousSessionRecoveryKey = sessionStorage.getItem(sessionStorageKey); +sessionStorage.removeItem(sessionStorageKey); +if (previousSessionRecoveryKey) { + clientOpts.recover = previousSessionRecoveryKey; +} + +const client = new Ably.Realtime(clientOpts); + +// Store recovery data when page is being closed or refreshed +window.addEventListener('beforeunload', () => { + const recoveryKey = client.connection.createRecoveryKey(); + sessionStorage.setItem(sessionStorageKey, recoveryKey); +}); +``` + +```realtime_nodejs +// This pattern is specific to browser environments with sessionStorage +// Node.js applications would need alternative storage mechanisms +``` + + + + Alternatively, if it is necessary to be explicit about the connection `recoveryKey`, the connection can be recovered by providing the last value of the connection's `recoveryKey` value in the [client options](/docs/api/realtime-sdk#client-options) `recover` attribute when instantiating an SDK. ## Handling connection failures @@ -408,7 +464,7 @@ While an SDK will not automatically attempt to reconnect in the `FAILED` state, Other classes of error are nonfatal. For example, a client may have network connectivity issues. An SDK will attempt to automatically reconnect and recover from these sort of issues, as detailed in the `DISCONNECTED` and `SUSPENDED` explanations in the [available connection states](#connection-states) section. -If message continuity is lost in the process, e.g. because you have been disconnected from Ably for more than two minutes, the SDK will notify you through the [`resumed`](#resume) flag mechanism. +If message continuity is lost in the process, for example, because you have been disconnected from Ably for more than two minutes, the SDK will notify you through the [`resumed`](#resume) flag mechanism. ## Mobile app lifecycle management diff --git a/src/pages/docs/platform/errors/index.mdx b/src/pages/docs/platform/errors/index.mdx index 1747e54eee..02cb6ee386 100644 --- a/src/pages/docs/platform/errors/index.mdx +++ b/src/pages/docs/platform/errors/index.mdx @@ -13,6 +13,103 @@ You can debug issues in your Ably-supported app using the following: * Meta channels allow you to monitor errors that might not otherwise be visible to clients, providing additional insights into issues. * The [Dev console](https://ably.com/accounts/any/apps/any/console) in your Ably dashboard is a quick and easy way to inspect errors and events, especially during development or debugging. +## Monitoring best practices + +### Use SDK mechanisms for error detection + +Ably SDKs provide built-in mechanisms to notify you when errors occur: + +* Connection state changes - The connection will emit events when it changes state. If it enters the `disconnected` or `failed` state, the state change will include a `reason` explaining why. In the `disconnected` state, it will automatically retry after 15 seconds. +* Channel state changes - Channel state works similarly, with different states and automatic retry behavior. +* Client library logs - ERROR level logs can be accessed programmatically using a custom log handler. + +### Avoid monitoring HTTP status codes + +Strongly recommended against monitoring HTTP status codes of individual requests to detect problems. An HTTP request with a non-2xx response does not necessarily indicate a problem. + +There are many possible reasons individual HTTP requests may return error status codes, especially with imperfect network connections, and only a small fraction represent actual problems. For example: + +* When a token expires and cannot complete online reauth in time, a comet receive stream will close with a 401 status code and error code 40142. This is expected behavior that triggers the library to obtain a new token and resume the connection. +* Many actual problems won't show up as non-2xx HTTP requests (e.g., errors sent as protocol messages down a WebSocket). + +The Ably protocol was designed to use semantically appropriate status codes for each response, rather than avoiding non-2xx responses except for genuine unexpected conditions. + +Recommended approach: Use the built-in SDK mechanisms, particularly [connection state changes](/docs/connect/states#listen) and [channel state changes](/docs/channels/states#listen), for reliable error detection and monitoring. + +## SSL certificate issues + +### Common SSL certificate errors + +If you encounter SSL certificate errors when connecting to Ably, you may see errors such as: + +``` +cURL error: SSL certificate problem: self signed certificate in certificate chain +SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed +SSL: CERTIFICATE_VERIFY_FAILED certificate verify failed: unable to get local issuer certificate +``` + +These errors are usually caused by out-of-date root certificates on your server or local machine. + +### Root cause + +SSL certificate verification failures typically occur when: + +- Your system's root certificate authority (CA) certificates are outdated +- The certificate chain cannot be properly verified +- Your system lacks the necessary intermediate certificates + +### Solutions + +#### Update root certificates + +The most common solution is to update your system's root certificates: + +On Ubuntu/Debian: +```bash +sudo apt-get update && sudo apt-get install ca-certificates +``` + +On CentOS/RHEL: +```bash +sudo yum update ca-certificates +``` + +On macOS: +```bash +brew install ca-certificates +# Or update existing: +brew upgrade ca-certificates +``` + +On Windows: +Windows typically updates root certificates through Windows Update. Ensure your system is up to date. + +#### Language-specific solutions + +Different programming languages may require additional steps: + +PHP: Update your `curl.cainfo` setting in `php.ini` or download the latest CA bundle from curl.se + +Ruby: Update your Ruby installation or use the `certified` gem to ensure proper certificate verification + +Node.js: Update Node.js to the latest version, which includes updated root certificates + +Python: Update the `certifi` package which provides Mozilla's root certificates + +#### Alternative approaches + +If updating root certificates doesn't resolve the issue: + +1. Check your firewall/proxy: Corporate networks may intercept SSL connections +2. Verify system time: Ensure your system clock is accurate, as certificate validation depends on correct time +3. Test with curl: Use `curl -I https://rest.ably.io` to test SSL connectivity directly + + + +If you continue experiencing SSL certificate issues after trying these solutions, [contact Ably support](https://ably.com/support) with details about your platform, programming language, and the specific error messages you're seeing. + ## Error info All errors returned by Ably are standardized and use the [`ErrorInfo`](/docs/api/rest-sdk/types#error-info) object: