aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matt Kane <m@mk.gg> 2025-04-14 10:01:09 +0100
committerGravatar GitHub <noreply@github.com> 2025-04-14 10:01:09 +0100
commit2fd6a6b7aa51a4713af7fac37d5dfd824543c1bc (patch)
treea1ccf5b6a1d3afd330d65bb803e4a352174585d5
parent3e0f6aedc04367e6cb894ba37ff4a8de0e1f71f3 (diff)
downloadastro-2fd6a6b7aa51a4713af7fac37d5dfd824543c1bc.tar.gz
astro-2fd6a6b7aa51a4713af7fac37d5dfd824543c1bc.tar.zst
astro-2fd6a6b7aa51a4713af7fac37d5dfd824543c1bc.zip
feat: stable sessions (#13527)
Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> Co-authored-by: florian-lefebvre <69633530+florian-lefebvre@users.noreply.github.com> Co-authored-by: sarah11918 <5098874+sarah11918@users.noreply.github.com> Co-authored-by: ematipico <602478+ematipico@users.noreply.github.com>
-rw-r--r--.changeset/all-readers-jump.md89
-rw-r--r--packages/astro/src/core/config/schemas/base.ts2
-rw-r--r--packages/astro/src/core/errors/errors-data.ts52
-rw-r--r--packages/astro/src/core/logger/core.ts1
-rw-r--r--packages/astro/src/core/render-context.ts43
-rw-r--r--packages/astro/src/core/session.ts32
-rw-r--r--packages/astro/src/integrations/hooks.ts6
-rw-r--r--packages/astro/src/types/public/config.ts206
-rw-r--r--packages/astro/src/vite-plugin-astro-server/plugin.ts2
-rw-r--r--packages/astro/test/sessions.test.js86
-rw-r--r--packages/astro/test/units/sessions/astro-session.test.js23
-rw-r--r--packages/integrations/cloudflare/src/index.ts4
-rw-r--r--packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs3
-rw-r--r--packages/integrations/netlify/src/index.ts4
-rw-r--r--packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs5
-rw-r--r--packages/integrations/netlify/test/functions/sessions.test.js3
-rw-r--r--packages/integrations/node/src/index.ts4
-rw-r--r--packages/integrations/node/test/sessions.test.js6
18 files changed, 356 insertions, 215 deletions
diff --git a/.changeset/all-readers-jump.md b/.changeset/all-readers-jump.md
new file mode 100644
index 000000000..f6dbe24dd
--- /dev/null
+++ b/.changeset/all-readers-jump.md
@@ -0,0 +1,89 @@
+---
+'@astrojs/cloudflare': minor
+'@astrojs/netlify': minor
+'@astrojs/node': minor
+'astro': minor
+---
+
+The experimental session API introduced in Astro 5.1 is now stable and ready for production use.
+
+Sessions are used to store user state between requests for [on-demand rendered pages](https://astro.build/en/guides/on-demand-rendering/). You can use them to store user data, such as authentication tokens, shopping cart contents, or any other data that needs to persist across requests:
+
+```astro
+---
+export const prerender = false; // Not needed with 'server' output
+const cart = await Astro.session.get('cart');
+---
+
+<a href="/checkout">🛒 {cart?.length ?? 0} items</a>
+```
+
+## Configuring session storage
+
+Sessions require a storage driver to store the data. The Node, Cloudflare and Netlify adapters automatically configure a default driver for you, but other adapters currently require you to specify a custom storage driver in your configuration.
+
+If you are using an adapter that doesn't have a default driver, or if you want to choose a different driver, you can configure it using the `session` configuration option:
+
+```js
+import { defineConfig } from 'astro/config';
+import vercel from '@astrojs/vercel';
+
+export default defineConfig({
+ adapter: vercel(),
+ session: {
+ driver: 'upstash',
+ },
+});
+```
+
+## Using sessions
+
+Sessions are available in on-demand rendered pages, API endpoints, actions and middleware.
+
+In pages and components, you can access the session using `Astro.session`:
+
+```astro
+---
+const cart = await Astro.session.get('cart');
+---
+
+<a href="/checkout">🛒 {cart?.length ?? 0} items</a>
+```
+
+In endpoints, actions, and middleware, you can access the session using `context.session`:
+
+```js
+export async function GET(context) {
+ const cart = await context.session.get('cart');
+ return Response.json({ cart });
+}
+```
+
+If you attempt to access the session when there is no storage driver configured, or in a prerendered page, the session object will be `undefined` and an error will be logged in the console:
+
+```astro
+---
+export const prerender = true;
+const cart = await Astro.session?.get('cart'); // Logs an error. Astro.session is undefined
+---
+```
+
+## Upgrading from Experimental to Stable
+
+If you were previously using the experimental API, please remove the `experimental.session` flag from your configuration:
+
+```diff
+import { defineConfig } from 'astro/config';
+import node from '@astrojs/node';
+
+export default defineConfig({
+ adapter: node({
+ mode: "standalone",
+ }),
+- experimental: {
+- session: true,
+- },
+});
+```
+
+See [the sessions guide](https://docs.astro.build/en/guides/sessions/) for more information.
diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts
index 1776785b9..2bfbf9e5c 100644
--- a/packages/astro/src/core/config/schemas/base.ts
+++ b/packages/astro/src/core/config/schemas/base.ts
@@ -96,7 +96,6 @@ export const ASTRO_CONFIG_DEFAULTS = {
responsiveImages: false,
svg: false,
serializeConfig: false,
- session: false,
headingIdCompat: false,
preserveScriptOrder: false,
},
@@ -464,7 +463,6 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.responsiveImages),
- session: z.boolean().optional(),
svg: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.svg),
serializeConfig: z
.boolean()
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index db2a841b8..687c0ab90 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -1827,24 +1827,9 @@ export const ActionsCantBeLoaded = {
// Session Errors
/**
* @docs
- * @see
- * - [Server output adapter feature](https://docs.astro.build/en/reference/adapter-reference/#building-an-adapter)
- * @description
- * Your adapter must support server output to use sessions.
- */
-export const SessionWithoutSupportedAdapterOutputError = {
- name: 'SessionWithoutSupportedAdapterOutputError',
- title: "Sessions cannot be used with an adapter that doesn't support server output.",
- message:
- 'Sessions require an adapter that supports server output. The adapter must set `"server"` in the `buildOutput` adapter feature.',
- hint: 'Ensure your adapter supports `buildOutput: "server"`: https://docs.astro.build/en/reference/adapter-reference/#building-an-adapter',
-} satisfies ErrorData;
-
-/**
- * @docs
* @message Error when initializing session storage with driver `DRIVER`. `ERROR`
* @see
- * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
+ * - [Sessions](https://docs.astro.build/en/guides/sessions/)
* @description
* Thrown when the session storage could not be initialized.
*/
@@ -1853,14 +1838,14 @@ export const SessionStorageInitError = {
title: 'Session storage could not be initialized.',
message: (error: string, driver?: string) =>
`Error when initializing session storage${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
- hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
+ hint: 'For more information, see https://docs.astro.build/en/guides/sessions/',
} satisfies ErrorData;
/**
* @docs
* @message Error when saving session data with driver `DRIVER`. `ERROR`
* @see
- * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
+ * - [Sessions](https://docs.astro.build/en/guides/sessions/)
* @description
* Thrown when the session data could not be saved.
*/
@@ -1869,14 +1854,30 @@ export const SessionStorageSaveError = {
title: 'Session data could not be saved.',
message: (error: string, driver?: string) =>
`Error when saving session data${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``,
- hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/',
+ hint: 'For more information, see https://docs.astro.build/en/guides/sessions/',
} satisfies ErrorData;
/**
* @docs
- * @message The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage
* @see
- * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
+ * - [Sessions](https://docs.astro.build/en/guides/sessions/)
+ * @deprecated This error was removed in Astro 5.7, when the Sessions feature stopped being experimental.
+ * @description
+ * Your adapter must support server output to use sessions.
+ */
+export const SessionWithoutSupportedAdapterOutputError = {
+ name: 'SessionWithoutSupportedAdapterOutputError',
+ title: "Sessions cannot be used with an adapter that doesn't support server output.",
+ message:
+ 'Sessions require an adapter that supports server output. The adapter must set `"server"` in the `buildOutput` adapter feature.',
+ hint: 'Ensure your adapter supports `buildOutput: "server"`: https://docs.astro.build/en/reference/adapter-reference/#building-an-adapter',
+} satisfies ErrorData;
+/**
+ * @docs
+ * @message The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage.
+ * @deprecated This error was removed in Astro 5.7, when the Sessions feature stopped being experimental.
+ * @see
+ * - [Sessions](https://docs.astro.build/en/guides/sessions/)
* @description
* Thrown when session storage is enabled but not configured.
*/
@@ -1885,14 +1886,15 @@ export const SessionConfigMissingError = {
title: 'Session storage was enabled but not configured.',
message:
'The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage',
- hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/',
-} satisfies ErrorData;
+ hint: 'For more information, see https://docs.astro.build/en/guides/sessions/',
+ } satisfies ErrorData;
/**
* @docs
* @message Session config was provided without enabling the `experimental.session` flag
+ * @deprecated This error was removed in Astro 5.7, when the Sessions feature stopped being experimental.
* @see
- * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/)
+ * - [Sessions](https://docs.astro.build/en/guides/sessions/)
* @description
* Thrown when session storage is configured but the `experimental.session` flag is not enabled.
*/
@@ -1900,7 +1902,7 @@ export const SessionConfigWithoutFlagError = {
name: 'SessionConfigWithoutFlagError',
title: 'Session flag not set',
message: 'Session config was provided without enabling the `experimental.session` flag',
- hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/',
+ hint: 'For more information, see https://docs.astro.build/en/guides/sessions/',
} satisfies ErrorData;
/*
diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts
index e5c91f653..9d8c7e357 100644
--- a/packages/astro/src/core/logger/core.ts
+++ b/packages/astro/src/core/logger/core.ts
@@ -28,6 +28,7 @@ export type LoggerLabel =
| 'preferences'
| 'redirects'
| 'sync'
+ | 'session'
| 'toolbar'
| 'assets'
| 'env'
diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts
index 828090fa5..07462db56 100644
--- a/packages/astro/src/core/render-context.ts
+++ b/packages/astro/src/core/render-context.ts
@@ -1,3 +1,4 @@
+import { green } from 'kleur/colors';
import type { ActionAPIContext } from '../actions/runtime/utils.js';
import { getActionContext } from '../actions/runtime/virtual/server.js';
import { deserializeActionResult } from '../actions/runtime/virtual/shared.js';
@@ -59,7 +60,7 @@ export class RenderContext {
public props: Props = {},
public partial: undefined | boolean = undefined,
public session: AstroSession | undefined = pipeline.manifest.sessionConfig
- ? new AstroSession(cookies, pipeline.manifest.sessionConfig)
+ ? new AstroSession(cookies, pipeline.manifest.sessionConfig, pipeline.runtimeMode)
: undefined,
) {}
@@ -335,7 +336,7 @@ export class RenderContext {
createActionAPIContext(): ActionAPIContext {
const renderContext = this;
- const { cookies, params, pipeline, url, session } = this;
+ const { cookies, params, pipeline, url } = this;
const generator = `Astro v${ASTRO_VERSION}`;
const rewrite = async (reroutePayload: RewritePayload) => {
@@ -373,7 +374,23 @@ export class RenderContext {
get originPathname() {
return getOriginPathname(renderContext.request);
},
- session,
+ get session() {
+ if (this.isPrerendered) {
+ pipeline.logger.warn(
+ 'session',
+ `context.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered routes. If you need access to sessions, make sure that the route is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your routes server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`,
+ );
+ return undefined;
+ }
+ if (!renderContext.session) {
+ pipeline.logger.warn(
+ 'session',
+ `context.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`,
+ );
+ return undefined;
+ }
+ return renderContext.session;
+ },
};
}
@@ -512,7 +529,7 @@ export class RenderContext {
apiContext: ActionAPIContext,
): Omit<AstroGlobal, 'props' | 'self' | 'slots'> {
const renderContext = this;
- const { cookies, locals, params, pipeline, url, session } = this;
+ const { cookies, locals, params, pipeline, url } = this;
const { response } = result;
const redirect = (path: string, status = 302) => {
// If the response is already sent, error as we cannot proceed with the redirect.
@@ -536,7 +553,23 @@ export class RenderContext {
routePattern: this.routeData.route,
isPrerendered: this.routeData.prerender,
cookies,
- session,
+ get session() {
+ if (this.isPrerendered) {
+ pipeline.logger.warn(
+ 'session',
+ `Astro.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered pages. If you need access to sessions, make sure that the page is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your pages server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`,
+ );
+ return undefined;
+ }
+ if (!renderContext.session) {
+ pipeline.logger.warn(
+ 'session',
+ `Astro.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`,
+ );
+ return undefined;
+ }
+ return renderContext.session;
+ },
get clientAddress() {
return renderContext.getClientAddress();
},
diff --git a/packages/astro/src/core/session.ts b/packages/astro/src/core/session.ts
index f86fa536b..653514889 100644
--- a/packages/astro/src/core/session.ts
+++ b/packages/astro/src/core/session.ts
@@ -6,21 +6,15 @@ import {
builtinDrivers,
createStorage,
} from 'unstorage';
-import type { AstroSettings } from '../types/astro.js';
import type {
ResolvedSessionConfig,
+ RuntimeMode,
SessionConfig,
SessionDriverName,
} from '../types/public/config.js';
import type { AstroCookies } from './cookies/cookies.js';
import type { AstroCookieSetOptions } from './cookies/cookies.js';
-import {
- SessionConfigMissingError,
- SessionConfigWithoutFlagError,
- SessionStorageInitError,
- SessionStorageSaveError,
- SessionWithoutSupportedAdapterOutputError,
-} from './errors/errors-data.js';
+import { SessionStorageInitError, SessionStorageSaveError } from './errors/errors-data.js';
import { AstroError } from './errors/index.js';
export const PERSIST_SYMBOL = Symbol();
@@ -84,6 +78,7 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
cookie: cookieConfig = DEFAULT_COOKIE_NAME,
...config
}: Exclude<ResolvedSessionConfig<TDriver>, undefined>,
+ runtimeMode?: RuntimeMode
) {
this.#cookies = cookies;
let cookieConfigObject: AstroCookieSetOptions | undefined;
@@ -96,7 +91,7 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
}
this.#cookieConfig = {
sameSite: 'lax',
- secure: true,
+ secure: runtimeMode === 'production',
path: '/',
...cookieConfigObject,
httpOnly: true,
@@ -523,22 +518,3 @@ export async function resolveSessionDriver(driver: string | undefined): Promise<
return driver;
}
-
-export function validateSessionConfig(settings: AstroSettings): void {
- const { experimental, session } = settings.config;
- const { buildOutput } = settings;
- let error: AstroError | undefined;
- if (experimental.session) {
- if (!session?.driver) {
- error = new AstroError(SessionConfigMissingError);
- } else if (buildOutput === 'static') {
- error = new AstroError(SessionWithoutSupportedAdapterOutputError);
- }
- } else if (session?.driver) {
- error = new AstroError(SessionConfigWithoutFlagError);
- }
- if (error) {
- error.stack = undefined;
- throw error;
- }
-}
diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts
index cd0315db8..cdf4eb80b 100644
--- a/packages/astro/src/integrations/hooks.ts
+++ b/packages/astro/src/integrations/hooks.ts
@@ -16,7 +16,6 @@ import { mergeConfig } from '../core/config/index.js';
import { validateConfigRefined } from '../core/config/validate.js';
import { validateSetAdapter } from '../core/dev/adapter-validation.js';
import type { AstroIntegrationLogger, Logger } from '../core/logger/core.js';
-import { validateSessionConfig } from '../core/session.js';
import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
import type {
@@ -416,11 +415,6 @@ export async function runHookConfigDone({
}),
});
}
- // Session config is validated after all integrations have had a chance to
- // register a default session driver, and we know the output type.
- // This can't happen in the Zod schema because it that happens before adapters run
- // and also doesn't know whether it's a server build or static build.
- validateSessionConfig(settings);
}
export async function runHookServerSetup({
diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts
index bcfd0e43f..6bd9c6b99 100644
--- a/packages/astro/src/types/public/config.ts
+++ b/packages/astro/src/types/public/config.ts
@@ -297,7 +297,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
* '/news': {
* status: 302,
* destination: 'https://example.com/news'
- * },
+ * },
* // '/product1/', '/product1' // Note, this is not supported
* }
* })
@@ -579,36 +579,6 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
/**
* @docs
- * @name session
- * @type {SessionConfig}
- * @version 5.3.0
- * @description
- *
- * Configures experimental session support by specifying a storage `driver` as well as any associated `options`.
- * You must enable the `experimental.session` flag to use this feature.
- * Some adapters may provide a default session driver, but you can override it with your own configuration.
- *
- * You can specify [any driver from Unstorage](https://unstorage.unjs.io/drivers) or provide a custom config which will override your adapter's default.
- *
- * See [the experimental session guide](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information.
- *
- * ```js title="astro.config.mjs"
- * {
- * session: {
- * // Required: the name of the Unstorage driver
- * driver: 'redis',
- * // The required options depend on the driver
- * options: {
- * url: process.env.REDIS_URL,
- * },
- * }
- * }
- * ```
- */
- session?: SessionConfig<TSession>;
-
- /**
- * @docs
* @name vite
* @typeraw {ViteUserConfig}
* @description
@@ -964,6 +934,151 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
/**
* @docs
* @kind heading
+ * @version 5.7.0
+ * @name Session Options
+ * @description
+ *
+ * Configures session storage for your Astro project. This is used to store session data in a persistent way, so that it can be accessed across different requests.
+ * Some adapters may provide a default session driver, but you can override it with your own configuration.
+ *
+ * See [the sessions guide](https://docs.astro.build/en/guides/sessions/) for more information.
+ *
+ * ```js title="astro.config.mjs"
+ * {
+ * session: {
+ * // The name of the Unstorage driver
+ * driver: 'redis',
+ * // The required options depend on the driver
+ * options: {
+ * url: process.env.REDIS_URL,
+ * },
+ * ttl: 3600, // 1 hour
+ * }
+ * }
+ * ```
+ */
+ session?: SessionConfig<TSession>;
+
+ /**
+ * @docs
+ * @name session.driver
+ * @type {string | undefined}
+ * @version 5.7.0
+ * @description
+ *
+ * The Unstorage driver to use for session storage. The [Node](https://docs.astro.build/en/guides/integrations-guide/node/#sessions),
+ * [Cloudflare](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#sessions), and
+ * [Netlify](/en/guides/integrations-guide/netlify/#sessions) adapters automatically configure a default driver for you,
+ * but you can specify your own if you would prefer or if you are using an adapter that does not provide one.
+ *
+ * The value is the "Driver name" from the [Unstorage driver documentation](https://unstorage.unjs.io/drivers).
+ *
+ * ```js title="astro.config.mjs" ins={4}
+ * {
+ * adapter: vercel(),
+ * session: {
+ * driver: "redis",
+ * },
+ * }
+ * ```
+ * :::note
+ * Some drivers may need extra packages to be installed. Some drivers may also require environment variables or credentials to be set. See the [Unstorage documentation](https://unstorage.unjs.io/drivers) for more information.
+ * :::
+ *
+ */
+
+ /**
+ * @docs
+ * @name session.options
+ * @type {Record<string, unknown> | undefined}
+ * @version 5.7.0
+ * @default `{}`
+ * @description
+ *
+ * The driver-specific options to use for session storage. The options depend on the driver you are using. See the [Unstorage documentation](https://unstorage.unjs.io/drivers)
+ * for more information on the options available for each driver.
+ *
+ * ```js title="astro.config.mjs" ins={4-6}
+ * {
+ * session: {
+ * driver: "redis",
+ * options: {
+ * url: process.env.REDIS_URL
+ * },
+ * }
+ * }
+ * ```
+ */
+
+ /**
+ * @docs
+ * @name session.cookie
+ * @type {string | AstroCookieSetOptions | undefined}
+ * @version 5.7.0
+ * @default `{ name: "astro-session", sameSite: "lax", httpOnly: true, secure: true }`
+ * @description
+ *
+ * The session cookie configuration. If set to a string, it will be used as the cookie name.
+ * Alternatively, you can pass an object with additional options. These will be merged with the defaults.
+ *
+ * ```js title="astro.config.mjs" ins={3-4}
+ * {
+ * session: {
+ * // If set to a string, it will be used as the cookie name.
+ * cookie: "my-session-cookie",
+ * }
+ * }
+ *
+ * ```
+ *
+ * ```js title="astro.config.mjs" ins={4-8}
+ * {
+ * session: {
+ * // If set to an object, it will be used as the cookie options.
+ * cookie: {
+ * name: "my-session-cookie",
+ * sameSite: "lax",
+ * secure: true,
+ * }
+ * }
+ * }
+ * ```
+ */
+
+ /**
+ * @docs
+ * @name session.ttl
+ * @version 5.7.0
+ * @type {number | undefined}
+ * @default {Infinity}
+ * @description
+ *
+ * An optional default time-to-live expiration period for session values, in seconds.
+ *
+ * By default, session values persist until they are deleted or the session is destroyed, and do not automatically expire because a particular amount of time has passed.
+ * Set `session.ttl` to add a default expiration period for your session values. Passing a `ttl` option to [`session.set()`](https://docs.astro.build/en/reference/api-reference/#set) will override the global default
+ * for that individual entry.
+ *
+ * ```js title="astro.config.mjs" ins={3-4}
+ * {
+ * session: {
+ * // Set a default expiration period of 1 hour (3600 seconds)
+ * ttl: 3600,
+ * }
+ * }
+ * ```
+ * :::note
+ * Setting a value for `ttl` does not automatically delete the value from storage after the time limit has passed.
+ *
+ * Values from storage will only be deleted when there is an attempt to access them after the `ttl` period has expired. At this time, the session value will be undefined and only then will the value be deleted.
+ *
+ * Individual drivers may also support a `ttl` option that will automatically delete sessions after the specified time. See your chosen driver's documentation for more information.
+ * :::
+ */
+
+ /**
+ * @docs
+ * @kind heading
* @name Dev Toolbar Options
*/
devToolbar?: {
@@ -1950,7 +2065,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*
* ```js title=astro.config.mjs
* {
- * experimental: {
+ * experimental: {
* responsiveImages: true,
* },
* }
@@ -2058,33 +2173,6 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
/**
*
- * @name experimental.session
- * @type {boolean}
- * @default `false`
- * @version 5.0.0
- * @description
- *
- * Enables support for sessions in Astro. Sessions are used to store user data across requests, such as user authentication state.
- *
- * When enabled you can access the `Astro.session` object to read and write data that persists across requests. You can configure the session driver using the [`session` option](#session), or use the default provided by your adapter.
- *
- * ```astro title=src/components/CartButton.astro
- * ---
- * export const prerender = false; // Not needed in 'server' mode
- * const cart = await Astro.session.get('cart');
- * ---
- *
- * <a href="/checkout">🛒 {cart?.length ?? 0} items</a>
- *
- * ```
- *
- * For more details, see [the experimental session guide](https://docs.astro.build/en/reference/experimental-flags/sessions/).
- *
- */
-
- session?: boolean;
- /**
- *
* @name experimental.svg
* @type {boolean}
* @default `undefined`
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index bed0069b6..88b8b0a81 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -206,6 +206,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
onRequest: NOOP_MIDDLEWARE_FN,
};
},
- sessionConfig: settings.config.experimental.session ? settings.config.session : undefined,
+ sessionConfig: settings.config.session,
};
}
diff --git a/packages/astro/test/sessions.test.js b/packages/astro/test/sessions.test.js
index 9ac94c944..3fe385fb4 100644
--- a/packages/astro/test/sessions.test.js
+++ b/packages/astro/test/sessions.test.js
@@ -18,9 +18,6 @@ describe('Astro.session', () => {
driver: 'fs',
ttl: 20,
},
- experimental: {
- session: true,
- },
});
});
@@ -38,7 +35,7 @@ describe('Astro.session', () => {
}
it('can regenerate session cookies upon request', async () => {
- const firstResponse = await fetchResponse('/regenerate', { method: 'GET' });
+ const firstResponse = await fetchResponse('/regenerate');
const firstHeaders = Array.from(app.setCookieHeaders(firstResponse));
const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1];
@@ -53,8 +50,21 @@ describe('Astro.session', () => {
assert.notEqual(firstSessionId, secondSessionId);
});
+ it('defaults to secure cookies in production', async () => {
+ const firstResponse = await fetchResponse('/regenerate');
+ const firstHeaders = Array.from(app.setCookieHeaders(firstResponse));
+ assert.ok(
+ firstHeaders[0].includes('Secure'),
+ 'Secure cookie not set in production',
+ );
+ assert.ok(
+ firstHeaders[0].includes('HttpOnly'),
+ 'HttpOnly cookie not set in production',
+ );
+ });
+
it('can save session data by value', async () => {
- const firstResponse = await fetchResponse('/update', { method: 'GET' });
+ const firstResponse = await fetchResponse('/update');
const firstValue = await firstResponse.json();
assert.equal(firstValue.previousValue, 'none');
@@ -141,9 +151,6 @@ describe('Astro.session', () => {
driver: 'fs',
ttl: 20,
},
- experimental: {
- session: true,
- },
});
devServer = await fixture.startDevServer();
});
@@ -170,6 +177,13 @@ describe('Astro.session', () => {
assert.notEqual(firstSessionId, secondSessionId);
});
+
+ it('defaults to non-secure cookies in development', async () => {
+ const response = await fixture.fetch('/regenerate');
+ const setCookieHeader = response.headers.get('set-cookie');
+ assert.ok(!setCookieHeader.includes('Secure'));
+ });
+
it('can save session data by value', async () => {
const firstResponse = await fixture.fetch('/update');
const firstValue = await firstResponse.json();
@@ -219,60 +233,4 @@ describe('Astro.session', () => {
);
});
});
-
- describe('Configuration', () => {
- it('throws if flag is enabled but driver is not set', async () => {
- const fixture = await loadFixture({
- root: './fixtures/sessions/',
- output: 'server',
- adapter: testAdapter(),
- experimental: {
- session: true,
- },
- });
- await assert.rejects(
- fixture.build({}),
- /Error: The `experimental.session` flag was set to `true`, but no storage was configured/,
- );
- });
-
- it('throws if session is configured but flag is not enabled', async () => {
- const fixture = await loadFixture({
- root: './fixtures/sessions/',
- output: 'server',
- adapter: testAdapter(),
- session: {
- driver: 'fs',
- },
- experimental: {
- session: false,
- },
- });
- await assert.rejects(
- fixture.build({}),
- /Error: Session config was provided without enabling the `experimental.session` flag/,
- );
- });
-
- it('throws if output is static', async () => {
- const fixture = await loadFixture({
- root: './fixtures/sessions/',
- output: 'static',
- session: {
- driver: 'fs',
- ttl: 20,
- },
- experimental: {
- session: true,
- },
- });
- // Disable actions so we can do a static build
- await fixture.editFile('src/actions/index.ts', () => '');
- await assert.rejects(
- fixture.build({}),
- /Sessions require an adapter that supports server output/,
- );
- await fixture.resetAllFiles();
- });
- });
});
diff --git a/packages/astro/test/units/sessions/astro-session.test.js b/packages/astro/test/units/sessions/astro-session.test.js
index 3fa1b9de1..a7d65f215 100644
--- a/packages/astro/test/units/sessions/astro-session.test.js
+++ b/packages/astro/test/units/sessions/astro-session.test.js
@@ -18,13 +18,13 @@ const defaultConfig = {
};
// Helper to create a new session instance with mocked dependencies
-function createSession(config = defaultConfig, cookies = defaultMockCookies, mockStorage) {
+function createSession(config = defaultConfig, cookies = defaultMockCookies, mockStorage, runtimeMode = 'production') {
if (mockStorage) {
config.driver = 'test';
config.options ??= {};
config.options.mockStorage = mockStorage;
}
- return new AstroSession(cookies, config);
+ return new AstroSession(cookies, config, runtimeMode);
}
test('AstroSession - Basic Operations', async (t) => {
@@ -380,7 +380,7 @@ test('AstroSession - Cookie Security', async (t) => {
assert.equal(cookieOptions.httpOnly, true);
});
- await t.test('should set secure and sameSite by default', async () => {
+ await t.test('should set secure and sameSite by default in production', async () => {
let cookieOptions;
const mockCookies = {
...defaultMockCookies,
@@ -395,6 +395,23 @@ test('AstroSession - Cookie Security', async (t) => {
assert.equal(cookieOptions.secure, true);
assert.equal(cookieOptions.sameSite, 'lax');
});
+
+ await t.test('should set secure to false in development', async () => {
+ let cookieOptions;
+ const mockCookies = {
+ ...defaultMockCookies,
+ set: (_name, _value, options) => {
+ cookieOptions = options;
+ },
+ };
+
+ const session = createSession(defaultConfig, mockCookies, undefined, 'development');
+
+ session.set('key', 'value');
+ assert.equal(cookieOptions.secure, false);
+ assert.equal(cookieOptions.sameSite, 'lax');
+ })
+
});
test('AstroSession - Storage Errors', async (t) => {
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index cb275eae0..d5c2e0be9 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -145,11 +145,11 @@ export default function createIntegration(args?: Options): AstroIntegration {
const isBuild = command === 'build';
- if (config.experimental.session && !session?.driver) {
+ if (!session?.driver) {
const sessionDir = isBuild ? undefined : createCodegenDir();
const bindingName = args?.sessionKVBindingName ?? 'SESSION';
logger.info(
- `Configuring experimental session support using ${isBuild ? 'Cloudflare KV' : 'filesystem storage'}. Be sure to define a KV binding named "${bindingName}".`,
+ `Enabling sessions with ${isBuild ? 'Cloudflare KV' : 'filesystem storage'}. Be sure to define a KV binding named "${bindingName}".`,
);
logger.info(
`If you see the error "Invalid binding \`${bindingName}\`" in your build output, you need to add the binding to your wrangler config file.`,
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs
index 7d37e9762..b5f6fc107 100644
--- a/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs
@@ -9,7 +9,4 @@ export default defineConfig({
enabled: true,
},
}),
- experimental: {
- session: true,
- }
});
diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts
index 3a447f5fd..b2c020e70 100644
--- a/packages/integrations/netlify/src/index.ts
+++ b/packages/integrations/netlify/src/index.ts
@@ -521,9 +521,9 @@ export default function netlifyIntegration(
let session = config.session;
- if (config.experimental.session && !session?.driver) {
+ if (!session?.driver) {
logger.info(
- `Configuring experimental session support using ${isRunningInNetlify ? 'Netlify Blobs' : 'filesystem storage'}`,
+ `Enabling sessions with ${isRunningInNetlify ? 'Netlify Blobs' : 'filesystem storage'}`,
);
session = isRunningInNetlify
? {
diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs
index 23d03b44f..f7ac423c9 100644
--- a/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs
+++ b/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs
@@ -4,8 +4,5 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'server',
adapter: netlify(),
- site: `http://example.com`,
- experimental: {
- session: true,
- }
+ site: `http://example.com`
});
diff --git a/packages/integrations/netlify/test/functions/sessions.test.js b/packages/integrations/netlify/test/functions/sessions.test.js
index 767895e5c..107e32190 100644
--- a/packages/integrations/netlify/test/functions/sessions.test.js
+++ b/packages/integrations/netlify/test/functions/sessions.test.js
@@ -40,9 +40,6 @@ describe('Astro.session', () => {
root: new URL('./fixtures/sessions/', import.meta.url),
output: 'server',
adapter: netlify(),
- experimental: {
- session: true,
- },
// @ts-ignore
session: { driver: '', options },
});
diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts
index e1cf5e8b5..26ffd2ad0 100644
--- a/packages/integrations/node/src/index.ts
+++ b/packages/integrations/node/src/index.ts
@@ -37,8 +37,8 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr
'astro:config:setup': async ({ updateConfig, config, logger }) => {
let session = config.session;
- if (config.experimental.session && !session?.driver) {
- logger.info('Configuring experimental session support using filesystem storage');
+ if (!session?.driver) {
+ logger.info('Enabling sessions with filesystem storage');
session = {
...session,
driver: 'fs-lite',
diff --git a/packages/integrations/node/test/sessions.test.js b/packages/integrations/node/test/sessions.test.js
index e3cac3f11..431363744 100644
--- a/packages/integrations/node/test/sessions.test.js
+++ b/packages/integrations/node/test/sessions.test.js
@@ -15,9 +15,6 @@ describe('Astro.session', () => {
root: './fixtures/sessions/',
output: 'server',
adapter: nodejs({ mode: 'middleware' }),
- experimental: {
- session: true,
- },
});
});
@@ -109,9 +106,6 @@ describe('Astro.session', () => {
root: './fixtures/sessions/',
output: 'server',
adapter: nodejs({ mode: 'middleware' }),
- experimental: {
- session: true,
- },
});
devServer = await fixture.startDevServer();
});