UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

458 lines (392 loc) 12.6 kB
/** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */ /** @import { RemoteFormInternals, RemoteFunctionResponse, RemoteInternals, RequestState, SSROptions } from 'types' */ import { json, error } from '@sveltejs/kit'; import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal'; import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; import { app_dir, base } from '$app/paths/internal/server'; import { is_form_content_type } from '../../utils/http.js'; import { parse_remote_arg, split_remote_key, stringify } from '../shared.js'; import { handle_error_and_jsonify } from './utils.js'; import { normalize_error } from '../../utils/error.js'; import { check_incorrect_fail_use } from './page/actions.js'; import { DEV } from 'esm-env'; import { record_span } from '../telemetry/record_span.js'; import { deserialize_binary_form } from '../form-utils.js'; /** @type {typeof handle_remote_call_internal} */ export async function handle_remote_call(event, state, options, manifest, id) { return record_span({ name: 'sveltekit.remote.call', attributes: { 'sveltekit.remote.call.id': id }, fn: (current) => { const traced_event = merge_tracing(event, current); return with_request_store({ event: traced_event, state }, () => handle_remote_call_internal(traced_event, state, options, manifest, id) ); } }); } /** * @param {RequestEvent} event * @param {RequestState} state * @param {SSROptions} options * @param {SSRManifest} manifest * @param {string} id */ async function handle_remote_call_internal(event, state, options, manifest, id) { const [hash, name, additional_args] = id.split('/'); const remotes = manifest._.remotes; if (!remotes[hash]) error(404); const module = await remotes[hash](); const fn = module.default[name]; if (!fn) error(404); /** @type {RemoteInternals} */ const internals = fn.__; const transport = options.hooks.transport; event.tracing.current.setAttributes({ 'sveltekit.remote.call.type': internals.type, 'sveltekit.remote.call.name': internals.name }); try { if (internals.type === 'query_batch') { if (event.request.method !== 'POST') { throw new SvelteKitError( 405, 'Method Not Allowed', `\`query.batch\` functions must be invoked via POST request, not ${event.request.method}` ); } /** @type {{ payloads: string[] }} */ const { payloads } = await event.request.json(); const args = await Promise.all( payloads.map((payload) => parse_remote_arg(payload, transport)) ); const results = await with_request_store({ event, state }, () => internals.run(args, options) ); return json( /** @type {RemoteFunctionResponse} */ ({ type: 'result', result: stringify(results, transport) }) ); } if (internals.type === 'form') { if (event.request.method !== 'POST') { throw new SvelteKitError( 405, 'Method Not Allowed', `\`form\` functions must be invoked via POST request, not ${event.request.method}` ); } if (!is_form_content_type(event.request)) { throw new SvelteKitError( 415, 'Unsupported Media Type', `\`form\` functions expect form-encoded data — received ${event.request.headers.get( 'content-type' )}` ); } const { data, meta, form_data } = await deserialize_binary_form(event.request); state.remote.requested = create_requested_map(meta.remote_refreshes); // If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set) // Note that additional_args will only be set if the form is not enhanced, as enhanced forms transfer the key inside `data`. if (additional_args && !('id' in data)) { data.id = JSON.parse(decodeURIComponent(additional_args)); } const fn = internals.fn; const result = await with_request_store({ event, state }, () => fn(data, meta, form_data)); return json( /** @type {RemoteFunctionResponse} */ ({ type: 'result', result: stringify(result, transport), refreshes: result.issues ? undefined : await serialize_singleflight(state.remote.refreshes), reconnects: result.issues ? undefined : await serialize_singleflight(state.remote.reconnects) }) ); } if (internals.type === 'command') { /** @type {{ payload: string, refreshes?: string[] }} */ const { payload, refreshes } = await event.request.json(); state.remote.requested = create_requested_map(refreshes); const arg = parse_remote_arg(payload, transport); const data = await with_request_store({ event, state }, () => fn(arg)); return json( /** @type {RemoteFunctionResponse} */ ({ type: 'result', result: stringify(data, transport), refreshes: await serialize_singleflight(state.remote.refreshes), reconnects: await serialize_singleflight(state.remote.reconnects) }) ); } if (internals.type === 'query_live') { if (event.request.method !== 'GET') { throw new SvelteKitError( 405, 'Method Not Allowed', `\`query.live\` functions must be invoked via GET request, not ${event.request.method}` ); } const payload = /** @type {string} */ ( new URL(event.request.url).searchParams.get('payload') ); const generator = internals.run(event, state, parse_remote_arg(payload, transport)); const encoder = new TextEncoder(); /** * @param {ReadableStreamDefaultController} controller * @param {any} payload */ function send(controller, payload) { controller.enqueue(encoder.encode(JSON.stringify(payload) + '\n')); } let closed = false; /** @type {string | undefined} */ let result = undefined; async function cancel() { if (closed) return; closed = true; await generator.return(undefined); } event.request.signal.addEventListener('abort', cancel, { once: true }); return new Response( new ReadableStream({ async pull(controller) { if (event.request.signal.aborted) { await cancel(); controller.close(); return; } try { while (true) { const { value, done } = await generator.next(); if (done) { await cancel(); controller.close(); return; } // only send changed data if (result !== (result = stringify(value, transport))) { send(controller, { type: 'result', result }); return; } } } catch (error) { if (!event.request.signal.aborted) { if (error instanceof Redirect) { send(controller, { type: 'redirect', location: error.location }); } else { const status = error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500; send(controller, { type: 'error', error: await handle_error_and_jsonify(event, state, options, error), status }); } } await cancel(); controller.close(); } }, cancel }), { headers: { 'cache-control': 'private, no-store', 'content-type': 'application/x-ndjson' } } ); } const payload = internals.type === 'prerender' ? additional_args : /** @type {string} */ ( // new URL(...) necessary because we're hiding the URL from the user in the event object new URL(event.request.url).searchParams.get('payload') ); const data = await with_request_store({ event, state }, () => fn(parse_remote_arg(payload, transport)) ); return json( /** @type {RemoteFunctionResponse} */ ({ type: 'result', result: stringify(data, transport) }) ); } catch (error) { if (error instanceof Redirect) { return json( /** @type {RemoteFunctionResponse} */ ({ type: 'redirect', location: error.location, refreshes: await serialize_singleflight(state.remote.refreshes), reconnects: await serialize_singleflight(state.remote.reconnects) }) ); } const status = error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500; return json( /** @type {RemoteFunctionResponse} */ ({ type: 'error', error: await handle_error_and_jsonify(event, state, options, error), status }), { // By setting a non-200 during prerendering we fail the prerender process (unless handleHttpError handles it). // Errors at runtime will be passed to the client and are handled there status: state.prerendering ? status : undefined, headers: { 'cache-control': 'private, no-store' } } ); } /** @param {Map<string, Promise<any>> | null} map */ async function serialize_singleflight(map) { if (!map || map.size === 0) { return undefined; } const results = await Promise.all( Array.from(map, async ([key, promise]) => { try { return [key, { type: 'result', data: await promise }]; } catch (error) { const status = error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500; return [ key, { type: 'error', status, error: await handle_error_and_jsonify(event, state, options, error) } ]; } }) ); return stringify(Object.fromEntries(results), transport); } } /** * @param {string[] | undefined} refreshes */ function create_requested_map(refreshes) { /** @type {Map<string, string[]>} */ const requested = new Map(); for (const key of refreshes ?? []) { const parts = split_remote_key(key); const existing = requested.get(parts.id); if (existing) { existing.push(parts.payload); } else { requested.set(parts.id, [parts.payload]); } } return requested; } /** @type {typeof handle_remote_form_post_internal} */ export async function handle_remote_form_post(event, state, manifest, id) { return record_span({ name: 'sveltekit.remote.form.post', attributes: { 'sveltekit.remote.form.post.id': id }, fn: (current) => { const traced_event = merge_tracing(event, current); return with_request_store({ event: traced_event, state }, () => handle_remote_form_post_internal(traced_event, state, manifest, id) ); } }); } /** * @param {RequestEvent} event * @param {RequestState} state * @param {SSRManifest} manifest * @param {string} id * @returns {Promise<ActionResult>} */ async function handle_remote_form_post_internal(event, state, manifest, id) { const [hash, name, action_id] = id.split('/'); const remotes = manifest._.remotes; const module = await remotes[hash]?.(); let form = /** @type {RemoteForm<any, any>} */ (module?.default[name]); if (!form) { event.setHeaders({ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 // "The server must generate an Allow header field in a 405 status code response" allow: 'GET' }); return { type: 'error', error: new SvelteKitError( 405, 'Method Not Allowed', `POST method not allowed. No form actions exist for ${DEV ? `the page at ${event.route.id}` : 'this page'}` ) }; } if (action_id) { // @ts-expect-error form = with_request_store({ event, state }, () => form.for(JSON.parse(action_id))); } try { const fn = /** @type {RemoteFormInternals} */ (/** @type {any} */ (form).__).fn; const { data, meta, form_data } = await deserialize_binary_form(event.request); if (action_id && !('id' in data)) { data.id = JSON.parse(decodeURIComponent(action_id)); } await with_request_store({ event, state }, () => fn(data, meta, form_data)); // We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it. // It is instead available on `myForm.result`, setting of which happens within the remote `form` function. return { type: 'success', status: 200 }; } catch (e) { const err = normalize_error(e); if (err instanceof Redirect) { return { type: 'redirect', status: err.status, location: err.location }; } return { type: 'error', error: check_incorrect_fail_use(err) }; } } /** * @param {URL} url */ export function get_remote_id(url) { return ( url.pathname.startsWith(`${base}/${app_dir}/remote/`) && url.pathname.replace(`${base}/${app_dir}/remote/`, '') ); } /** * @param {URL} url */ export function get_remote_action(url) { return url.searchParams.get('/remote'); }