Skip to content
5 changes: 5 additions & 0 deletions .changeset/quiet-pumas-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: allow handle functions composed with sequence to run without a request store
81 changes: 41 additions & 40 deletions packages/kit/src/exports/hooks/sequence.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
/** @import { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit' */
/** @import { RecordSpan } from 'types' */
import {
merge_tracing,
get_request_store,
try_get_request_store,
with_request_store
} from '@sveltejs/kit/internal/server';
import { noop_span } from '../../runtime/telemetry/noop.js';

/** @type {RecordSpan} */
const noop_record_span = ({ fn }) => fn(noop_span);

/**
* A helper function for sequencing multiple `handle` calls in a middleware-like manner.
Expand Down Expand Up @@ -80,11 +85,11 @@ import {
*/
export function sequence(...handlers) {
const length = handlers.length;
if (!length) return ({ event, resolve }) => resolve(event);

return ({ event, resolve }) => {
const { state } = get_request_store();
return apply_handle(0, event, {});
const store = try_get_request_store();
const record_span = store ? store.state.tracing.record_span : noop_record_span;
return apply_handle(0, event, undefined);

/**
* @param {number} i
Expand All @@ -93,51 +98,47 @@ export function sequence(...handlers) {
* @returns {Promise<Response>}
*/
function apply_handle(i, event, parent_options) {
if (i === length) return resolve(event, parent_options);

const handle = handlers[i];

return state.tracing.record_span({
return record_span({
name: `sveltekit.handle.sequenced.${handle.name ? handle.name : i}`,
attributes: {},
fn: async (current) => {
const traced_event = merge_tracing(event, current);
return await with_request_store({ event: traced_event, state }, () =>
handle({
event: traced_event,
resolve: (event, options) => {
/** @type {ResolveOptions['transformPageChunk']} */
const transformPageChunk = async ({ html, done }) => {
if (options?.transformPageChunk) {
html = (await options.transformPageChunk({ html, done })) ?? '';
}

if (parent_options?.transformPageChunk) {
html = (await parent_options.transformPageChunk({ html, done })) ?? '';
}

return html;
};
return await with_request_store(
store && { event: traced_event, state: store.state },
() =>
handle({
event: traced_event,
resolve: (event, options) => {
/** @type {ResolveOptions} */
const merged = {
transformPageChunk:
options?.transformPageChunk || parent_options?.transformPageChunk
? async ({ html, done }) => {
if (options?.transformPageChunk) {
html = (await options.transformPageChunk({ html, done })) ?? '';
}

/** @type {ResolveOptions['filterSerializedResponseHeaders']} */
const filterSerializedResponseHeaders =
parent_options?.filterSerializedResponseHeaders ??
options?.filterSerializedResponseHeaders;
if (parent_options?.transformPageChunk) {
html =
(await parent_options.transformPageChunk({ html, done })) ?? '';
}

/** @type {ResolveOptions['preload']} */
const preload = parent_options?.preload ?? options?.preload;
return html;
}
: undefined,
filterSerializedResponseHeaders:
parent_options?.filterSerializedResponseHeaders ??
options?.filterSerializedResponseHeaders,
preload: parent_options?.preload ?? options?.preload
};

return i < length - 1
? apply_handle(i + 1, event, {
transformPageChunk,
filterSerializedResponseHeaders,
preload
})
: resolve(event, {
transformPageChunk,
filterSerializedResponseHeaders,
preload
});
}
})
return apply_handle(i + 1, event, merged);
}
})
);
}
});
Expand Down
44 changes: 18 additions & 26 deletions packages/kit/src/exports/hooks/sequence.spec.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
/** @import { RequestEvent } from '@sveltejs/kit' */
/** @import { RequestState } from 'types' */
import { assert, expect, test, vi } from 'vitest';
import { assert, expect, test } from 'vitest';
import { sequence } from './sequence.js';
import { noop_span } from '../../runtime/telemetry/noop.js';

const dummy_event = vi.hoisted(
() =>
/** @type {RequestEvent} */ ({
tracing: {
root: {}
}
})
);

vi.mock(import('@sveltejs/kit/internal/server'), async (actualPromise) => {
const actual = await actualPromise();
return {
...actual,
get_request_store: () => ({
event: dummy_event,
state: /** @type {RequestState} */ ({
tracing: {
record_span: ({ fn }) => fn(noop_span)
}
})
})
};

const dummy_event = /** @type {RequestEvent} */ ({
tracing: {
root: {}
}
});

test('runs without a request store', async () => {
const handler = sequence(
async ({ event, resolve }) => resolve(event),
async ({ event, resolve }) => resolve(event)
);

const response = new Response();
const event = /** @type {RequestEvent} */ ({});

assert.equal(await handler({ event, resolve: () => Promise.resolve(response) }), response);
});

test('applies handlers in sequence', async () => {
Expand Down
Loading