From 4bf15ae6f304b9aa49a4396c3f51dc423d0722dc Mon Sep 17 00:00:00 2001 From: Bao Nguyen <39545125+giaBaoJS@users.noreply.github.com> Date: Tue, 1 Sep 2026 03:23:00 +0700 Subject: [PATCH] fix: throw when `setContext` is called after an `await` during SSR (#18739) Fixes #17233. --- .changeset/olive-mice-argue.md | 5 +++++ .../98-reference/.generated/client-errors.md | 8 -------- .../98-reference/.generated/shared-errors.md | 8 ++++++++ packages/svelte/messages/client-errors/errors.md | 6 ------ packages/svelte/messages/shared-errors/errors.md | 6 ++++++ packages/svelte/src/internal/client/errors.js | 16 ---------------- packages/svelte/src/internal/server/context.js | 11 +++++++++-- packages/svelte/src/internal/server/renderer.js | 13 ++++++++++--- packages/svelte/src/internal/server/types.d.ts | 2 ++ packages/svelte/src/internal/shared/errors.js | 16 ++++++++++++++++ .../async-context-throws-after-await/_config.js | 3 +-- 11 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 .changeset/olive-mice-argue.md diff --git a/.changeset/olive-mice-argue.md b/.changeset/olive-mice-argue.md new file mode 100644 index 0000000000..6d749bcc3b --- /dev/null +++ b/.changeset/olive-mice-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 2ab442afc3..3f37be269d 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -225,14 +225,6 @@ Rest element properties of `$props()` such as `%property%` are readonly The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files ``` -### set_context_after_init - -``` -`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression -``` - -This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. - ### state_descriptors_fixed ``` diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 44616d7c8d..db8c45c0c7 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -80,6 +80,14 @@ Context was not set in the current component or any of its ancestors The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. +### set_context_after_init + +``` +`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression +``` + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ### snippet_without_render_tag ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 3b00d1516b..85bdd68010 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -171,12 +171,6 @@ This can happen if you render a hydratable on the client that was not rendered o > The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files -## set_context_after_init - -> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression - -This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. - ## state_descriptors_fixed > Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index e005e34fc8..acb59b4190 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -66,6 +66,12 @@ Certain lifecycle methods can only be used during component initialisation. To f The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. +## set_context_after_init + +> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ## snippet_without_render_tag > Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index d60c2dd280..175bb2e528 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -429,22 +429,6 @@ export function rune_outside_svelte(rune) { } } -/** - * `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression - * @returns {never} - */ -export function set_context_after_init() { - if (DEV) { - const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`); - - error.name = 'Svelte error'; - - throw error; - } else { - throw new Error(`https://svelte.dev/e/set_context_after_init`); - } -} - /** * Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. * @returns {never} diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index b204c4c138..80d437907e 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -1,6 +1,7 @@ /** @import { SSRContext } from '#server' */ import { DEV } from 'esm-env'; import { create_context, get_or_init_context_map } from '../shared/context.js'; +import * as e from './errors.js'; /** @type {SSRContext | null} */ export var ssr_context = null; @@ -40,7 +41,13 @@ export function getContext(key) { * @returns {T} */ export function setContext(key, context) { - get_or_init_context_map(ssr_context, 'setContext').set(key, context); + const context_map = get_or_init_context_map(ssr_context, 'setContext'); + + if (/** @type {SSRContext} */ (ssr_context).i) { + e.set_context_after_init(); + } + + context_map.set(key, context); return context; } @@ -61,7 +68,7 @@ export function getAllContexts() { * @param {Function} [fn] */ export function push(fn) { - ssr_context = { p: ssr_context, c: null, r: null }; + ssr_context = { p: ssr_context, c: null, r: null, i: false }; if (DEV) { ssr_context.function = fn; diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index a30c302693..6b3c1b28dd 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -162,6 +162,11 @@ export class Renderer { let promise = Promise.resolve(thunks[0]()); const promises = [promise]; + if (context !== null && thunks.length > 1) { + // the remaining thunks run after an `await`, by which point it is too late to set context + context.i = true; + } + for (const fn of thunks.slice(1)) { promise = promise.then(() => { const previous_context = ssr_context; @@ -209,7 +214,8 @@ export class Renderer { ...ssr_context, p: parent, c: null, - r: child + r: child, + i: ssr_context?.i ?? false }); const result = fn(child); @@ -260,7 +266,8 @@ export class Renderer { ...ssr_context, p: parent_context, c: null, - r: child + r: child, + i: ssr_context?.i ?? false }); try { @@ -859,7 +866,7 @@ export class Renderer { try { /** @type {SSRContext} */ - const context = { p: null, c: options.context ?? null, r: renderer }; + const context = { p: null, c: options.context ?? null, r: renderer, i: false }; set_ssr_context(context); renderer.push(BLOCK_OPEN); diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 899255d366..0e8a4cb678 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -9,6 +9,8 @@ export interface SSRContext { c: null | Map; /** renderer */ r: null | Renderer; + /** True if initialized, i.e. an `await` was reached */ + i: boolean; /** dev mode only: the current component function */ function?: any; /** dev mode only: the current element */ diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 9e41788dc1..6cbbe82ca5 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -101,6 +101,22 @@ export function missing_context() { } } +/** + * `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression + * @returns {never} + */ +export function set_context_after_init() { + if (DEV) { + const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/set_context_after_init`); + } +} + /** * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. * @returns {never} diff --git a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js index 2d1b6be570..85622d23d3 100644 --- a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js +++ b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js @@ -1,7 +1,6 @@ import { test } from '../../test'; export default test({ - skip: true, // TODO it appears there might be an actual bug here; the promise isn't ever actually awaited in spite of being awaited in the component mode: ['async'], - error: 'lifecycle_outside_component' + error: 'set_context_after_init' });