diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts index b5692de059..73ad1c262f 100644 --- a/e2e/react-start/server-functions/src/routeTree.gen.ts +++ b/e2e/react-start/server-functions/src/routeTree.gen.ts @@ -27,6 +27,7 @@ import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index' import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index' import { Route as FactoryIndexRouteImport } from './routes/factory/index' import { Route as CookiesIndexRouteImport } from './routes/cookies/index' +import { Route as MiddlewareUnhandledExceptionRouteImport } from './routes/middleware/unhandled-exception' import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn' import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware' import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router' @@ -123,6 +124,12 @@ const CookiesIndexRoute = CookiesIndexRouteImport.update({ path: '/cookies/', getParentRoute: () => rootRouteImport, } as any) +const MiddlewareUnhandledExceptionRoute = + MiddlewareUnhandledExceptionRouteImport.update({ + id: '/middleware/unhandled-exception', + path: '/middleware/unhandled-exception', + getParentRoute: () => rootRouteImport, + } as any) const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({ id: '/middleware/send-serverFn', path: '/middleware/send-serverFn', @@ -170,6 +177,7 @@ export interface FileRoutesByFullPath { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies': typeof CookiesIndexRoute '/factory': typeof FactoryIndexRoute '/formdata-redirect': typeof FormdataRedirectIndexRoute @@ -195,6 +203,7 @@ export interface FileRoutesByTo { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies': typeof CookiesIndexRoute '/factory': typeof FactoryIndexRoute '/formdata-redirect': typeof FormdataRedirectIndexRoute @@ -221,6 +230,7 @@ export interface FileRoutesById { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies/': typeof CookiesIndexRoute '/factory/': typeof FactoryIndexRoute '/formdata-redirect/': typeof FormdataRedirectIndexRoute @@ -248,6 +258,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/unhandled-exception' | '/cookies' | '/factory' | '/formdata-redirect' @@ -273,6 +284,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/unhandled-exception' | '/cookies' | '/factory' | '/formdata-redirect' @@ -298,6 +310,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/unhandled-exception' | '/cookies/' | '/factory/' | '/formdata-redirect/' @@ -324,6 +337,7 @@ export interface RootRouteChildren { MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute + MiddlewareUnhandledExceptionRoute: typeof MiddlewareUnhandledExceptionRoute CookiesIndexRoute: typeof CookiesIndexRoute FactoryIndexRoute: typeof FactoryIndexRoute FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute @@ -460,6 +474,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CookiesIndexRouteImport parentRoute: typeof rootRouteImport } + '/middleware/unhandled-exception': { + id: '/middleware/unhandled-exception' + path: '/middleware/unhandled-exception' + fullPath: '/middleware/unhandled-exception' + preLoaderRoute: typeof MiddlewareUnhandledExceptionRouteImport + parentRoute: typeof rootRouteImport + } '/middleware/send-serverFn': { id: '/middleware/send-serverFn' path: '/middleware/send-serverFn' @@ -516,6 +537,7 @@ const rootRouteChildren: RootRouteChildren = { MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute, MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute, MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute, + MiddlewareUnhandledExceptionRoute: MiddlewareUnhandledExceptionRoute, CookiesIndexRoute: CookiesIndexRoute, FactoryIndexRoute: FactoryIndexRoute, FormdataRedirectIndexRoute: FormdataRedirectIndexRoute, diff --git a/e2e/react-start/server-functions/src/routes/index.tsx b/e2e/react-start/server-functions/src/routes/index.tsx index 596b855c09..ca1394015a 100644 --- a/e2e/react-start/server-functions/src/routes/index.tsx +++ b/e2e/react-start/server-functions/src/routes/index.tsx @@ -88,6 +88,11 @@ function Home() {
  • Server Functions Factory E2E tests
  • +
  • + + Server Functions Middleware Unhandled Exception E2E tests + +
  • ) diff --git a/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx b/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx new file mode 100644 index 0000000000..f6d3bb6017 --- /dev/null +++ b/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx @@ -0,0 +1,33 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createMiddleware, createServerFn } from '@tanstack/react-start' + +export const authMiddleware = createMiddleware({ type: 'function' }).server( + async ({ next, context }) => { + throw new Error('Unauthorized') + }, +) + +const personServerFn = createServerFn({ method: 'GET' }) + .middleware([authMiddleware]) + .inputValidator((d: string) => d) + .handler(({ data: name }) => { + return { name, randomNumber: Math.floor(Math.random() * 100) } + }) + +export const Route = createFileRoute('/middleware/unhandled-exception')({ + loader: async () => { + return { + person: await personServerFn({ data: 'John Doe' }), + } + }, + component: RouteComponent, +}) + +function RouteComponent() { + const { person } = Route.useLoaderData() + return ( +
    + {person.name} - {person.randomNumber} +
    + ) +} diff --git a/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx b/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx index 826ec255d3..5f9165adf7 100644 --- a/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx +++ b/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx @@ -33,7 +33,7 @@ function SubmitPostFormDataFn() {

    Submit POST FormData Fn Call

    - It should return navigate and return{' '} + It should navigate to a raw response of {''}
                 Hello, {testValues.name}!
    diff --git a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    index 5511bdc517..b1b781d7d0 100644
    --- a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    +++ b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    @@ -17,6 +17,16 @@ import type { Plugin as SerovalPlugin } from 'seroval'
     
     let serovalPlugins: Array> | null = null
     
    +// caller =>
    +//   serverFnFetcher =>
    +//     client =>
    +//       server =>
    +//         fn =>
    +//       seroval =>
    +//     client middleware =>
    +//   serverFnFetcher =>
    +// caller
    +
     export async function serverFnFetcher(
       url: string,
       args: Array,
    @@ -37,7 +47,8 @@ export async function serverFnFetcher(
     
         // Arrange the headers
         const headers = new Headers({
    -      'x-tsr-redirect': 'manual',
    +      'x-tsr-serverFn': 'true',
    +      'x-tsr-createServerFn': 'true',
           ...(first.headers instanceof Headers
             ? Object.fromEntries(first.headers.entries())
             : first.headers),
    @@ -65,12 +76,6 @@ export async function serverFnFetcher(
           }
         }
     
    -    if (url.includes('?')) {
    -      url += `&createServerFn`
    -    } else {
    -      url += `?createServerFn`
    -    }
    -
         let body = undefined
         if (first.method === 'POST') {
           const fetchBody = await getFetchBody(first)
    @@ -97,6 +102,7 @@ export async function serverFnFetcher(
         handler(url, {
           method: 'POST',
           headers: {
    +        'x-tsr-serverFn': 'true',
             Accept: 'application/json',
             'Content-Type': 'application/json',
           },
    @@ -165,7 +171,7 @@ async function getFetchBody(
     async function getResponse(fn: () => Promise) {
       const response = await (async () => {
         try {
    -      return await fn()
    +      return await fn() // client => server => fn => server => client
         } catch (error) {
           if (error instanceof Response) {
             return error
    @@ -178,22 +184,16 @@ async function getResponse(fn: () => Promise) {
       if (response.headers.get(X_TSS_RAW_RESPONSE) === 'true') {
         return response
       }
    +
       const contentType = response.headers.get('content-type')
       invariant(contentType, 'expected content-type header to be set')
       const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)
    -  // If the response is not ok, throw an error
    -  if (!response.ok) {
    -    if (serializedByStart && contentType.includes('application/json')) {
    -      const jsonPayload = await response.json()
    -      const result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
    -      throw result
    -    }
    -
    -    throw new Error(await response.text())
    -  }
     
    +  // If the response is serialized by the start server, we need to process it
    +  // differently than a normal response.
       if (serializedByStart) {
         let result
    +    // If it's a stream from the start serializer, process it as such
         if (contentType.includes('application/x-ndjson')) {
           const refs = new Map()
           result = await processServerFnResponse({
    @@ -206,17 +206,22 @@ async function getResponse(fn: () => Promise) {
             },
           })
         }
    +    // If it's a JSON response, it can be simpler
         if (contentType.includes('application/json')) {
           const jsonPayload = await response.json()
           result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
         }
    +
         invariant(result, 'expected result to be resolved')
         if (result instanceof Error) {
           throw result
         }
    +
         return result
       }
     
    +  // If it wasn't processed by the start serializer, check
    +  // if it's JSON
       if (contentType.includes('application/json')) {
         const jsonPayload = await response.json()
         const redirect = parseRedirect(jsonPayload)
    @@ -229,6 +234,12 @@ async function getResponse(fn: () => Promise) {
         return jsonPayload
       }
     
    +  // Othwerwise, if it's not OK, throw the content
    +  if (!response.ok) {
    +    throw new Error(await response.text())
    +  }
    +
    +  // Or return the response itself
       return response
     }
     
    diff --git a/packages/start-client-core/src/constants.ts b/packages/start-client-core/src/constants.ts
    index 1e541af323..4e0777068d 100644
    --- a/packages/start-client-core/src/constants.ts
    +++ b/packages/start-client-core/src/constants.ts
    @@ -6,4 +6,5 @@ export const TSS_SERVER_FUNCTION_FACTORY = Symbol.for(
     
     export const X_TSS_SERIALIZED = 'x-tss-serialized'
     export const X_TSS_RAW_RESPONSE = 'x-tss-raw'
    +export const X_TSS_CONTEXT = 'x-tss-context'
     export {}
    diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts
    index bc95a6b5b9..b472d88d79 100644
    --- a/packages/start-client-core/src/createServerFn.ts
    +++ b/packages/start-client-core/src/createServerFn.ts
    @@ -1,10 +1,9 @@
    -import { isNotFound, isRedirect } from '@tanstack/router-core'
     import { mergeHeaders } from '@tanstack/router-core/ssr/client'
     
    +import { isRedirect, parseRedirect } from '@tanstack/router-core'
     import { TSS_SERVER_FUNCTION_FACTORY } from './constants'
     import { getStartOptions } from './getStartOptions'
     import { getStartContextServerOnly } from './getStartContextServerOnly'
    -import type { TSS_SERVER_FUNCTION } from './constants'
     import type {
       AnyValidator,
       Constrain,
    @@ -16,6 +15,7 @@ import type {
       ValidateSerializableInput,
       Validator,
     } from '@tanstack/router-core'
    +import type { TSS_SERVER_FUNCTION } from './constants'
     import type {
       AnyFunctionMiddleware,
       AnyRequestMiddleware,
    @@ -112,17 +112,22 @@ export const createServerFn: CreateServerFn = (options, __opts) => {
           return Object.assign(
             async (opts?: CompiledFetcherFnOptions) => {
               // Start by executing the client-side middleware chain
    -          return executeMiddleware(resolvedMiddleware, 'client', {
    +          const result = await executeMiddleware(resolvedMiddleware, 'client', {
                 ...extractedFn,
                 ...newOptions,
                 data: opts?.data as any,
                 headers: opts?.headers,
                 signal: opts?.signal,
                 context: {},
    -          }).then((d) => {
    -            if (d.error) throw d.error
    -            return d.result
               })
    +
    +          const redirect = parseRedirect(result.error)
    +          if (redirect) {
    +            throw redirect
    +          }
    +
    +          if (result.error) throw result.error
    +          return result.result
             },
             {
               // This copies over the URL, function ID
    @@ -144,14 +149,18 @@ export const createServerFn: CreateServerFn = (options, __opts) => {
                   request: startContext.request,
                 }
     
    -            return executeMiddleware(resolvedMiddleware, 'server', ctx).then(
    -              (d) => ({
    -                // Only send the result and sendContext back to the client
    -                result: d.result,
    -                error: d.error,
    -                context: d.sendContext,
    -              }),
    -            )
    +            const result = await executeMiddleware(
    +              resolvedMiddleware,
    +              'server',
    +              ctx,
    +            ).then((d) => ({
    +              // Only send the result and sendContext back to the client
    +              result: d.result,
    +              error: d.error,
    +              context: d.sendContext,
    +            }))
    +
    +            return result
               },
             },
           ) as any
    @@ -180,7 +189,7 @@ export async function executeMiddleware(
         ...middlewares,
       ])
     
    -  const next: NextFn = async (ctx) => {
    +  const callNextMiddleware: NextFn = async (ctx) => {
         // Get the next middleware
         const nextMiddleware = flattenedMiddlewares.shift()
     
    @@ -189,50 +198,111 @@ export async function executeMiddleware(
           return ctx
         }
     
    -    if (
    -      'inputValidator' in nextMiddleware.options &&
    -      nextMiddleware.options.inputValidator &&
    -      env === 'server'
    -    ) {
    -      // Execute the middleware's input function
    -      ctx.data = await execValidator(
    -        nextMiddleware.options.inputValidator,
    -        ctx.data,
    -      )
    -    }
    +    // Execute the middleware
    +    try {
    +      if (
    +        'inputValidator' in nextMiddleware.options &&
    +        nextMiddleware.options.inputValidator &&
    +        env === 'server'
    +      ) {
    +        // Execute the middleware's input function
    +        ctx.data = await execValidator(
    +          nextMiddleware.options.inputValidator,
    +          ctx.data,
    +        )
    +      }
     
    -    let middlewareFn: MiddlewareFn | undefined = undefined
    -    if (env === 'client') {
    -      if ('client' in nextMiddleware.options) {
    -        middlewareFn = nextMiddleware.options.client as MiddlewareFn | undefined
    +      let middlewareFn: MiddlewareFn | undefined = undefined
    +      if (env === 'client') {
    +        if ('client' in nextMiddleware.options) {
    +          middlewareFn = nextMiddleware.options.client as
    +            | MiddlewareFn
    +            | undefined
    +        }
    +      }
    +      // env === 'server'
    +      else if ('server' in nextMiddleware.options) {
    +        middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
           }
    -    }
    -    // env === 'server'
    -    else if ('server' in nextMiddleware.options) {
    -      middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
    -    }
     
    -    if (middlewareFn) {
    -      // Execute the middleware
    -      return applyMiddleware(middlewareFn, ctx, async (newCtx) => {
    -        return next(newCtx).catch((error: any) => {
    -          if (isRedirect(error) || isNotFound(error)) {
    +      if (middlewareFn) {
    +        const userNext = async (
    +          userCtx: ServerFnMiddlewareResult | undefined = {} as any,
    +        ) => {
    +          // Return the next middleware
    +          const nextCtx = {
    +            ...ctx,
    +            ...userCtx,
    +            context: {
    +              ...ctx.context,
    +              ...userCtx.context,
    +            },
    +            sendContext: {
    +              ...ctx.sendContext,
    +              ...(userCtx.sendContext ?? {}),
    +            },
    +            headers: mergeHeaders(ctx.headers, userCtx.headers),
    +            result:
    +              userCtx.result !== undefined
    +                ? userCtx.result
    +                : userCtx instanceof Response
    +                  ? userCtx
    +                  : (ctx as any).result,
    +            error: userCtx.error ?? (ctx as any).error,
    +          }
    +
    +          try {
    +            return await callNextMiddleware(nextCtx)
    +          } catch (error: any) {
                 return {
    -              ...newCtx,
    +              ...nextCtx,
                   error,
                 }
               }
    +        }
     
    -          throw error
    -        })
    -      })
    -    }
    +        // Execute the middleware
    +        const result = await middlewareFn({
    +          ...ctx,
    +          next: userNext as any,
    +        } as any)
    +
    +        // If result is NOT a ctx object, we need to return it as
    +        // the { result }
    +        if (isRedirect(result)) {
    +          return {
    +            ...ctx,
    +            error: result,
    +          }
    +        }
     
    -    return next(ctx)
    +        if (result instanceof Response) {
    +          return {
    +            ...ctx,
    +            result,
    +          }
    +        }
    +
    +        if (!(result as any)) {
    +          throw new Error(
    +            'User middleware returned undefined. You must call next() or return a result in your middlewares.',
    +          )
    +        }
    +
    +        return result
    +      }
    +
    +      return callNextMiddleware(ctx)
    +    } catch (error: any) {
    +      return {
    +        ...ctx,
    +        error,
    +      }
    +    }
       }
     
       // Start the middleware chain
    -  return next({
    +  return callNextMiddleware({
         ...opts,
         headers: opts.headers || {},
         sendContext: opts.sendContext || {},
    @@ -628,41 +698,6 @@ export type MiddlewareFn = (
       },
     ) => Promise
     
    -export const applyMiddleware = async (
    -  middlewareFn: MiddlewareFn,
    -  ctx: ServerFnMiddlewareOptions,
    -  nextFn: NextFn,
    -) => {
    -  return middlewareFn({
    -    ...ctx,
    -    next: (async (
    -      userCtx: ServerFnMiddlewareResult | undefined = {} as any,
    -    ) => {
    -      // Return the next middleware
    -      return nextFn({
    -        ...ctx,
    -        ...userCtx,
    -        context: {
    -          ...ctx.context,
    -          ...userCtx.context,
    -        },
    -        sendContext: {
    -          ...ctx.sendContext,
    -          ...(userCtx.sendContext ?? {}),
    -        },
    -        headers: mergeHeaders(ctx.headers, userCtx.headers),
    -        result:
    -          userCtx.result !== undefined
    -            ? userCtx.result
    -            : userCtx instanceof Response
    -              ? userCtx
    -              : (ctx as any).result,
    -        error: userCtx.error ?? (ctx as any).error,
    -      })
    -    }) as any,
    -  } as any)
    -}
    -
     export function execValidator(
       validator: AnyValidator,
       input: unknown,
    diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx
    index 70a4692842..10a84f299b 100644
    --- a/packages/start-client-core/src/index.tsx
    +++ b/packages/start-client-core/src/index.tsx
    @@ -74,7 +74,6 @@ export type {
       RequiredFetcher,
     } from './createServerFn'
     export {
    -  applyMiddleware,
       execValidator,
       flattenMiddlewares,
       executeMiddleware,
    @@ -85,6 +84,7 @@ export {
       TSS_SERVER_FUNCTION,
       X_TSS_SERIALIZED,
       X_TSS_RAW_RESPONSE,
    +  X_TSS_CONTEXT,
     } from './constants'
     
     export type * from './serverRoute'
    diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts
    index d0fd84e8f4..ff7bd0f445 100644
    --- a/packages/start-server-core/src/createStartHandler.ts
    +++ b/packages/start-server-core/src/createStartHandler.ts
    @@ -268,16 +268,17 @@ export function createStartHandler(
           [...middlewares, requestHandlerMiddleware],
           {
             request,
    -
             context: requestOpts?.context || {},
           },
         )
     
         const response: Response = ctx.response
     
    +    console.log('response', response)
    +
         if (isRedirect(response)) {
           if (isResolvedRedirect(response)) {
    -        if (request.headers.get('x-tsr-redirect') === 'manual') {
    +        if (request.headers.get('x-tsr-createServerFn') === 'true') {
               return json(
                 {
                   ...response.options,
    @@ -318,7 +319,7 @@ export function createStartHandler(
           const router = await getRouter()
           const redirect = router.resolveRedirect(response)
     
    -      if (request.headers.get('x-tsr-redirect') === 'manual') {
    +      if (request.headers.get('x-tsr-createServerFn') === 'true') {
             return json(
               {
                 ...response.options,
    diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts
    index ec9042ec47..221ca5af00 100644
    --- a/packages/start-server-core/src/server-functions-handler.ts
    +++ b/packages/start-server-core/src/server-functions-handler.ts
    @@ -1,10 +1,11 @@
    -import { isNotFound } from '@tanstack/router-core'
    +import { isNotFound, isPlainObject } from '@tanstack/router-core'
     import invariant from 'tiny-invariant'
     import {
       TSS_FORMDATA_CONTEXT,
       X_TSS_RAW_RESPONSE,
       X_TSS_SERIALIZED,
       getDefaultSerovalPlugins,
    +  json,
     } from '@tanstack/start-client-core'
     import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
     import { getResponse } from './request-response'
    @@ -41,7 +42,9 @@ export const handleServerAction = async ({
         createServerFn?: boolean
       }
     
    -  const isCreateServerFn = 'createServerFn' in search
    +  const isServerFn = request.headers.get('x-tsr-serverFn') === 'true'
    +  const isCreateServerFn =
    +    request.headers.get('x-tsr-createServerFn') === 'true'
     
       if (typeof serverFnId !== 'string') {
         throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
    @@ -56,6 +59,9 @@ export const handleServerAction = async ({
       ]
     
       const contentType = request.headers.get('Content-Type')
    +  const isFormData = formDataContentTypes.some(
    +    (type) => contentType && contentType.includes(type),
    +  )
       const serovalPlugins = getDefaultSerovalPlugins()
     
       function parsePayload(payload: any) {
    @@ -65,13 +71,9 @@ export const handleServerAction = async ({
     
       const response = await (async () => {
         try {
    -      let result = await (async () => {
    +      let res = await (async () => {
             // FormData
    -        if (
    -          formDataContentTypes.some(
    -            (type) => contentType && contentType.includes(type),
    -          )
    -        ) {
    +        if (isFormData) {
               // We don't support GET requests with FormData payloads... that seems impossible
               invariant(
                 method.toLowerCase() !== 'get',
    @@ -135,25 +137,55 @@ export const handleServerAction = async ({
             return await action(...jsonPayload)
           })()
     
    -      // Any time we get a Response back, we should just
    -      // return it immediately.
    -      if (result.result instanceof Response) {
    -        result.result.headers.set(X_TSS_RAW_RESPONSE, 'true')
    -        return result.result
    -      }
    +      const isCtxResult =
    +        isPlainObject(res) &&
    +        'context' in res &&
    +        ('result' in res || 'error' in res)
    +
    +      console.log(
    +        {
    +          isServerFn,
    +          isCreateServerFn,
    +          isFormData,
    +          isCtxResult,
    +        },
    +        res,
    +      )
     
    -      // If this is a non createServerFn request, we need to
    -      // pull out the result from the result object
    -      if (!isCreateServerFn) {
    -        result = result.result
    +      function unwrapResultOrError(result: any) {
    +        if (
    +          isPlainObject(result) &&
    +          ('result' in result || 'error' in result)
    +        ) {
    +          console.log('tanner')
    +          return result.result || result.error
    +        }
    +        return result
    +      }
     
    -        // The result might again be a response,
    -        // and if it is, return it.
    -        if (result instanceof Response) {
    -          return result
    +      // This was not called by the serverFnFetcher, so it's likely a no-JS POST request)
    +      if (isCtxResult) {
    +        const unwrapped = unwrapResultOrError(res)
    +        if (unwrapped instanceof Response) {
    +          res = unwrapped
    +        } else {
    +          res = json(unwrapped)
             }
           }
     
    +      if (isNotFound(res)) {
    +        res = isNotFoundResponse(res)
    +      }
    +
    +      if (!isServerFn) {
    +        return res
    +      }
    +
    +      if (res instanceof Response) {
    +        res.headers.set(X_TSS_RAW_RESPONSE, 'true')
    +        return res
    +      }
    +
           // TODO: RSCs Where are we getting this package?
           // if (isValidElement(result)) {
           //   const { renderToPipeableStream } = await import(
    @@ -173,91 +205,91 @@ export const handleServerAction = async ({
           //   return new Response(null, { status: 200 })
           // }
     
    -      if (isNotFound(result)) {
    -        return isNotFoundResponse(result)
    -      }
    -
    -      const response = getResponse()
    -      let nonStreamingBody: any = undefined
    -
    -      if (result !== undefined) {
    -        // first run without the stream in case `result` does not need streaming
    -        let done = false as boolean
    -        const callbacks: {
    -          onParse: (value: any) => void
    -          onDone: () => void
    -          onError: (error: any) => void
    -        } = {
    -          onParse: (value) => {
    -            nonStreamingBody = value
    -          },
    -          onDone: () => {
    -            done = true
    -          },
    -          onError: (error) => {
    -            throw error
    -          },
    -        }
    -        toCrossJSONStream(result, {
    -          refs: new Map(),
    -          plugins: serovalPlugins,
    -          onParse(value) {
    -            callbacks.onParse(value)
    -          },
    -          onDone() {
    -            callbacks.onDone()
    -          },
    -          onError: (error) => {
    -            callbacks.onError(error)
    -          },
    -        })
    -        if (done) {
    -          return new Response(
    -            nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
    -            {
    -              status: response?.status,
    -              statusText: response?.statusText,
    -              headers: {
    -                'Content-Type': 'application/json',
    -                [X_TSS_SERIALIZED]: 'true',
    +      return serializeResult(res)
    +
    +      function serializeResult(res: unknown): Response {
    +        let nonStreamingBody: any = undefined
    +
    +        const alsResponse = getResponse()
    +        if (res !== undefined) {
    +          // first run without the stream in case `result` does not need streaming
    +          let done = false as boolean
    +          const callbacks: {
    +            onParse: (value: any) => void
    +            onDone: () => void
    +            onError: (error: any) => void
    +          } = {
    +            onParse: (value) => {
    +              nonStreamingBody = value
    +            },
    +            onDone: () => {
    +              done = true
    +            },
    +            onError: (error) => {
    +              throw error
    +            },
    +          }
    +          toCrossJSONStream(res, {
    +            refs: new Map(),
    +            plugins: serovalPlugins,
    +            onParse(value) {
    +              callbacks.onParse(value)
    +            },
    +            onDone() {
    +              callbacks.onDone()
    +            },
    +            onError: (error) => {
    +              callbacks.onError(error)
    +            },
    +          })
    +          if (done) {
    +            return new Response(
    +              nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
    +              {
    +                status: alsResponse?.status,
    +                statusText: alsResponse?.statusText,
    +                headers: {
    +                  'Content-Type': 'application/json',
    +                  [X_TSS_SERIALIZED]: 'true',
    +                },
                   },
    +            )
    +          }
    +
    +          // not done yet, we need to stream
    +          const stream = new ReadableStream({
    +            start(controller) {
    +              callbacks.onParse = (value) =>
    +                controller.enqueue(JSON.stringify(value) + '\n')
    +              callbacks.onDone = () => {
    +                try {
    +                  controller.close()
    +                } catch (error) {
    +                  controller.error(error)
    +                }
    +              }
    +              callbacks.onError = (error) => controller.error(error)
    +              // stream the initial body
    +              if (nonStreamingBody !== undefined) {
    +                callbacks.onParse(nonStreamingBody)
    +              }
                 },
    -          )
    +          })
    +          return new Response(stream, {
    +            status: alsResponse?.status,
    +            statusText: alsResponse?.statusText,
    +            headers: {
    +              'Content-Type': 'application/x-ndjson',
    +              [X_TSS_SERIALIZED]: 'true',
    +            },
    +          })
             }
     
    -        // not done yet, we need to stream
    -        const stream = new ReadableStream({
    -          start(controller) {
    -            callbacks.onParse = (value) =>
    -              controller.enqueue(JSON.stringify(value) + '\n')
    -            callbacks.onDone = () => {
    -              try {
    -                controller.close()
    -              } catch (error) {
    -                controller.error(error)
    -              }
    -            }
    -            callbacks.onError = (error) => controller.error(error)
    -            // stream the initial body
    -            if (nonStreamingBody !== undefined) {
    -              callbacks.onParse(nonStreamingBody)
    -            }
    -          },
    -        })
    -        return new Response(stream, {
    -          status: response?.status,
    -          statusText: response?.statusText,
    -          headers: {
    -            'Content-Type': 'application/x-ndjson',
    -            [X_TSS_SERIALIZED]: 'true',
    -          },
    +        return new Response(undefined, {
    +          status: alsResponse?.status,
    +          statusText: alsResponse?.statusText,
             })
           }
    -
    -      return new Response(undefined, {
    -        status: response?.status,
    -        statusText: response?.statusText,
    -      })
         } catch (error: any) {
           if (error instanceof Response) {
             return error