solidjs / solid-start

SolidStart, the Solid app framework
https://start.solidjs.com
MIT License
5.18k stars 375 forks source link

[Bug?]: createServerAction$, ReadableStream, and createEffect are inconsistent across dev and prod #935

Closed AustinGil closed 10 months ago

AustinGil commented 1 year ago

Duplicates

Latest version

Current behavior 😯

More context below, but the current behavior is that incrementally updating the content of a page using createEffect on a streaming response body works fine in dev mode, but breaks in production builds. In prod, the app waits until the request has completed before adding the entire response body to the page

Expected behavior 🤔

Production builds should work the same as dev builds. When creating an action that returns a streaming response, then watching for changes in the response with a createEffect, the client should append chunks of data to the page as the come in rather than wait for the entire response to complete.

Steps to reproduce 🕹

Steps:

  1. Create a component
  2. Add a server action that returns a stream
  3. Create a signal to track a string value
  4. create an effect that watches for changes on the server action
  5. Update the signal as chunks of data are read from the action
  6. Show the results on the page
  7. test app in dev mode
  8. test app in prod mode

Context 🔦

I have a route component that looks mostly like this, it creates a server action that makes an Axios request with the streaming configuration set to true (this part works fine). The action is submitted using the provided Form component, then I use createEffect to track the streaming response body content as it arrives.

In dev mode, it works as expected with each chunk being added to the page as intended. But when I build and run in prod, the app waits until the entire request is returned before adding the whole response body to the page.

This seems like a bug in the way createServerAction$ handles streams in Response objects, or maybe how createEffect treats the first item returned from the createServerAction$ array (prompt in this case).

import { createServerAction$ } from "solid-start/server";
import { createEffect, createSignal } from "solid-js";

export default function() {
  const [prompt, promptAction] = createServerAction$(async (formData) => {
    const stream = new ReadableStream({
      start(controller) {
        function merp(val) {
          console.log(val)
          if (val <= 0) {
            controller.close()
            return
          }
          controller.enqueue(val.toString())
          setTimeout(merp, 500, val - 1)
        }
        merp(4)
      }
    })

    return new Response(stream)
  })

  const [text, setText] = createSignal('')
  createEffect(async () => {
    if (!prompt.result) return

    const reader = prompt.result.body.getReader()
    const decoder = new TextDecoder()
    let processNextChunk = true

    while (processNextChunk) {
      const { value, done } = await reader.read()
      const chunkValue = decoder.decode(value)

      setText((previous) => previous + chunkValue)
      processNextChunk = !done
    }
  })

  return (
    <main>
      <promptAction.Form>
        <button type="submit">Submit</button>
      </promptAction.Form>

      <p>{text()}</p>
    </main>
  );
}

I've noticed the Request headers are almost identical across dev and prod, but the response headers are different.

Dev mode includes:

Prod mode includes:

Not sure if those matter. I could not set Content-Encoding: ''

Has anyone had success implementing streaming responses? Note that this does not use server sent events.

Your environment 🌎

Windows 10, WSL, Node 20, SolidStart 0.2.26
andrewrosss commented 1 year ago

Can you share which deployment platform you're using?

If it's vercel or netlify then the following could be the cause.

I was playing around with your example - the fact it works (at all!) in dev but not in prod (I experienced the same behaviour) makes me think it's the adapter(s) and not the server action code (maybe).

Both the vercel and netlify adapters call await webRes.text():

https://github.com/solidjs/solid-start/blob/8cfe543c18c2ee2aed30478da9893c37ec8be7f7/packages/start-vercel/entry.js#L43

https://github.com/solidjs/solid-start/blob/8cfe543c18c2ee2aed30478da9893c37ec8be7f7/packages/start-netlify/entry.js#L35

I'm not 100% sure what webRes is, but if it's a standard Response object then .text() does the following:

The text() method of the Response interface takes a Response stream and reads it to completion.

In which case, this could be the reason the stream is being consumed before making it to the client.

AustinGil commented 1 year ago

I'm just using Node (npm run build && npm start). Deployed it to a regular VPS, but the problem also exists if I run it locally on my computer.

andrewrosss commented 1 year ago

Hmm interesting, would you mind sharing your vite config?

AustinGil commented 1 year ago

Sure. I haven't changed anything from what came out of the generator:

import solid from "solid-start/vite";
import { defineConfig } from "vite";
export default defineConfig({
  plugins: [solid()],
});
ryansolid commented 10 months ago

I'm very excited in the changes we have made in the new Beta. I think this should work now as we can handle streamed responses in server functions. However,

In setting up for SolidStarts next Beta Phase built on Nitro and Vinxi we are closing all PRs/Issues that will not be merged due to the system changing. If you feel your issue was closed by mistake. Feel free to re-open it after updating/testing against 0.4.x release. Thank you for your patience.

See https://github.com/solidjs/solid-start/pull/1139 for more details.