Skip to content

HttpStatusProvider

olegivanov edited this page May 18, 2026 · 1 revision

HttpStatusProvider

1. Overview

  • Name: HttpStatusProvider
  • Purpose: Wrap an SSR tree with a render-scoped HttpStatusSink, exposed via context for descendant <HttpStatusCode> instances to write into.
  • When to use: Once per server-side render() call, around the root tree, before rendering. The SSR entry reads sink.code after await render() to set the HTTP response status.
  • Availability: All 5 framework adapters — @real-router/react/ssr, @real-router/preact/ssr, @real-router/solid/ssr, @real-router/vue/ssr, @real-router/svelte/ssr. Semantics identical across adapters; Svelte uses setContext.

2. Import and Basic Usage

<script lang="ts">
  import { HttpStatusProvider, createHttpStatusSink } from "@real-router/svelte/ssr";

  const sink = createHttpStatusSink();
</script>

<HttpStatusProvider {sink}>
  {#snippet children()}
    <App />
  {/snippet}
</HttpStatusProvider>

Descendant <HttpStatusCode code={404} /> writes 404 into sink.code. The SSR entry reads sink.code after await render(...):

const html = await render(App, { props: { sink } });
res.status(sink.code ?? 200).send(html);

3. Props

Prop Type Required Default Description
sink HttpStatusSink Yes Mutable sink to capture the status. Construct one per request via createHttpStatusSink().
children Snippet Yes Tree to render inside the provider.

4. Behavior

Provider mechanism

<script lang="ts">
  import { setContext } from "svelte";

  import { HTTP_STATUS_KEY } from "../context";

  setContext(HTTP_STATUS_KEY, sink);
</script>

{@render children()}

The provider calls setContext(HTTP_STATUS_KEY, sink) synchronously during init. Descendant <HttpStatusCode> instances read via getContext<HttpStatusSink | undefined>(HTTP_STATUS_KEY) and write sink.code = code once each.

Sink lifecycle (per-request only)

The sink is render-scoped. Construct one per request via createHttpStatusSink():

export async function handler(req, res) {
  const sink = createHttpStatusSink(); // fresh per request
  const html = await render(App, { props: { sink } });
  res.status(sink.code ?? 200).send(html);
}

Do NOT share a sink across requests. Module-level singletons would leak status between concurrent requests.

Don't Object.freeze the sink

<HttpStatusCode> writes sink.code = code during render. A frozen sink would throw TypeError: Cannot assign to read only property 'code' under ESM strict mode. Locked by property test tests/property/httpStatusSink.properties.ts Inv4.

Nested providers — innermost wins

getContext walks the context chain, finds the innermost matching key. Nesting providers is supported but uncommon — typical setup has one provider at the root:

<HttpStatusProvider sink={outerSink}>
  {#snippet children()}
    <!-- Outer sink available here. -->
    <HttpStatusProvider sink={innerSink}>
      {#snippet children()}
        <!-- Inner sink wins; <HttpStatusCode> writes here. -->
        <HttpStatusCode code={404} />  <!-- innerSink.code = 404 -->
      {/snippet}
    </HttpStatusProvider>
  {/snippet}
</HttpStatusProvider>

Tests pin nested-provider behavior — see tests/functional/HttpStatusCode.test.ts:82-91.

Server-only mounting is the common pattern

Typical SSR + CSR setup mounts the provider only on the server:

<!-- App.svelte -->
<script lang="ts">
  let { router, sink } = $props();
</script>

{#if sink}
  <HttpStatusProvider {sink}>
    {#snippet children()}
      <RootRoute {router} />
    {/snippet}
  </HttpStatusProvider>
{:else}
  <RootRoute {router} />
{/if}

Svelte 5's hydration walker tolerates the asymmetric {#if sink} branch — the server tree includes the provider, the client tree doesn't, and hydration succeeds without warnings. This is verified by examples/web/svelte/ssr-examples/ssr/ e2e tests.

Vue/Solid require symmetric provider mounting; Svelte's tolerance is a feature of its hydration walker.

5. Examples

Full SSR entry

// entry-server.ts
import { render } from "svelte/server";
import App from "./App.svelte";
import { createHttpStatusSink } from "@real-router/svelte/ssr";

import { createRouter } from "@real-router/core";

export async function handle({ event, resolve }) {
  const router = createRouter(routes);
  await router.start(event.url.pathname);

  const sink = createHttpStatusSink();
  const output = await render(App, {
    props: { router, sink },
  });

  return new Response(output.body, {
    status: sink.code ?? 200,
    headers: { "content-type": "text/html" },
  });
}
<!-- App.svelte -->
<script lang="ts">
  import {
    HttpStatusProvider,
    type HttpStatusSink,
  } from "@real-router/svelte/ssr";

  import { RouterProvider } from "@real-router/svelte";

  import RootRoutes from "./RootRoutes.svelte";

  import type { Router } from "@real-router/core";

  let {
    router,
    sink,
  }: { router: Router; sink?: HttpStatusSink } = $props();
</script>

{#if sink}
  <HttpStatusProvider {sink}>
    {#snippet children()}
      <RouterProvider {router}>
        <RootRoutes />
      </RouterProvider>
    {/snippet}
  </HttpStatusProvider>
{:else}
  <RouterProvider {router}>
    <RootRoutes />
  </RouterProvider>
{/if}

Conditional 404 inside a route component

<!-- routes/Product.svelte -->
<script lang="ts">
  import { HttpStatusCode } from "@real-router/svelte/ssr";

  import { useRoute } from "@real-router/svelte";

  const { route } = useRoute();
  const product = route.current.context.data?.product;
</script>

{#if !product}
  <HttpStatusCode code={404} />
  <h1>Product not found</h1>
{:else}
  <ProductPage {product} />
{/if}

Reusing the sink across multiple render passes (NOT supported)

// WRONG — sink is per-request.
const sink = createHttpStatusSink(); // module-level

export async function handler(req, res) {
  // sink leaks between concurrent requests.
  const html = await render(App, { props: { sink } });
  res.status(sink.code ?? 200).send(html);
}
// RIGHT — fresh sink per request.
export async function handler(req, res) {
  const sink = createHttpStatusSink();
  const html = await render(App, { props: { sink } });
  res.status(sink.code ?? 200).send(html);
}

6. Gotchas

  • Per-request only. Don't reuse the sink. Module-level singletons leak status between concurrent requests. Pinned by property test Inv1 (fresh per call, cross-write isolation).
  • Don't freeze. Object.freeze(sink) causes write failures under ESM strict mode.
  • Provider runs once at component init. Replacing sink mid-render isn't a supported pattern — the setContext call is captured once. To switch sinks, remount the provider.
  • Last write wins. Multiple <HttpStatusCode /> in the same render write to the same sink in template order; later writes overwrite earlier ones.
  • getContext is read by descendants once per instance. Once a <HttpStatusCode> reads the sink at init, it doesn't re-read on reactivity. Replace the sink → no effect on already-mounted <HttpStatusCode> instances.

7. Dependencies

  • HttpStatusSink type from @real-router/svelte/ssr (factory: createHttpStatusSink).
  • Svelte 5.x for setContext + runes.
  • No <RouterProvider> requirement — <HttpStatusProvider> is independent of routing.

8. Related

Navigation

Home


Concepts


Getting Started


Router Methods

Lifecycle

Navigation

State

URL & Path

Events


Standalone API

Tree-shakeable functions — import only what you need.

Routes — getRoutesApi(router)

Dependencies — getDependenciesApi(router)

Guards — getLifecycleApi(router)

Plugin Infrastructure — getPluginApi(router)

For plugin authors, not for general use.

SSR / SSG


React / Preact / Solid / Vue / Svelte Integration

Provider

Hooks

Components

SSR Components & Hooks

SSR-feature subpath — @real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.

  • Lazy (Svelte only — dynamic component import)
  • Await — read deferred SSR data by key
  • Streamed — Suspense-style boundary
  • ClientOnly — server fallback → client children swap
  • ServerOnly — server children, removed after hydration
  • HttpStatusCode — render-time HTTP status declaration
  • HttpStatusProvider — provides sink to descendant <HttpStatusCode>
  • useDeferred — read deferred Promise by key

DOM Utilities

Patterns


Subscription Layer (@real-router/sources)


Reactive Streams (@real-router/rx)


Plugins

Browser Plugin

Navigation Plugin

Hash Plugin

Memory Plugin

Lifecycle Plugin

Preload Plugin

Logger Plugin

Persistent Params

SSR Data

RSC Server

Validation

Search Schema

Utilities


Reference

Types

Error Codes

Clone this wiki locally