Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net::ERR_INCOMPLETE_CHUNKED_ENCODING when using tRPC subscriptions and the Hono trpc-server/ #717

Open
samuelgoldenbaum opened this issue Aug 27, 2024 · 1 comment

Comments

@samuelgoldenbaum
Copy link

samuelgoldenbaum commented Aug 27, 2024

tRPC subscriptions throw a net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK) error when using an httpSubscriptionLink that uses Server-sent Events (SSE) for subscriptions

image

This causes the client to continuously fire off new subscription calls.

This seems restricted to subscriptions so far as mutations and queries seem fine.

server.ts:

import { initTRPC } from '@trpc/server'
import { cors } from 'hono/cors'
import { trpcServer } from '@hono/trpc-server'
import { z } from 'zod'
import { EventEmitter, on } from 'events'
import { randomUUID } from 'crypto'
import superjson from 'superjson'
import { Hono } from 'hono'
import { EVENT, Widget } from '../common'

const t = initTRPC.create({
  transformer: superjson
})
const eventEmitter = new EventEmitter()

const publicProcedure = t.procedure
const router = t.router

const appRouter = router({
  create: publicProcedure
    .input(
      z.object({
        name: z.string()
      })
    )
    .mutation(({ input }) => {
      const widget: Widget = {
        ...input,
        id: randomUUID(),
        createdAt: new Date().toDateString()
      } satisfies Widget

      eventEmitter.emit(EVENT.CREATE, widget)
    }),
  onCreate: publicProcedure.subscription(async function* (opts) {
    for await (const [data] of on(eventEmitter, EVENT.CREATE)) {
      const widget = data as Widget
      yield widget
    }
  })
})

export type AppRouter = typeof appRouter

const app = new Hono().use(cors()).use(
  '*',
  trpcServer({
    router: appRouter
  })
)

export default {
  port: 3001,
  fetch: app.fetch
}

client:
trpc.ts

import {
  createTRPCClient,
  httpBatchLink,
  loggerLink,
  splitLink,
  unstable_httpSubscriptionLink
} from '@trpc/client'
import { AppRouter } from './../../hono-server'
import superjson from 'superjson'

const url = 'http://localhost:3001/trpc'

export const trpc = createTRPCClient<AppRouter>({
  links: [
    loggerLink(),
    splitLink({
      condition: (op) => op.type === 'subscription',
      true: unstable_httpSubscriptionLink({
        url,
        transformer: superjson
      }),
      false: httpBatchLink({
        url,
        transformer: superjson
      })
    })
  ]
})

App.tsx

import { trpc } from './tprc'
import React, { useEffect, useState } from 'react'
import './App.css'
import { faker } from '@faker-js/faker'
import { Widget } from '../../common'

function App() {
  const [widgets, setWidgets] = useState<Widget[]>([])

  useEffect(() => {
    trpc.onCreate.subscribe(undefined, {
      onData: (data) => {
        setWidgets((widgets) => [...widgets, data])
      },
      onError: (err) => {
        console.error('subscribe error', err)
      }
    })
  }, [])

  return (
    <div className="App">
      <header className="App-header">Widgets</header>

      <button
        onClick={() => {
          trpc.create.mutate({ name: faker.commerce.productName() })
        }}
      >
        Create Widget
      </button>
      <hr />
      <ul>
        {widgets.map((widget) => (
          <li key={widget.id}>{widget.name}</li>
        ))}
      </ul>
    </div>
  )
}

export default App

Running the same code using node HTTP server seems fine:

import { initTRPC } from '@trpc/server'
import { createHTTPServer } from '@trpc/server/adapters/standalone'
import cors from 'cors'
import { z } from 'zod'
import { EventEmitter, on } from 'events'
import { randomUUID } from 'crypto'
import superjson from 'superjson'
import { EVENT, Widget } from '../common'

const t = initTRPC.create({
  transformer: superjson
})
const eventEmitter = new EventEmitter()

const publicProcedure = t.procedure
const router = t.router

const appRouter = router({
  create: publicProcedure
    .input(
      z.object({
        name: z.string()
      })
    )
    .mutation(({ input }) => {
      const widget: Widget = {
        ...input,
        id: randomUUID(),
        createdAt: new Date().toDateString()
      } satisfies Widget

      eventEmitter.emit(EVENT.CREATE, widget)
    }),
  onCreate: publicProcedure.subscription(async function* (opts) {
    for await (const [data] of on(eventEmitter, EVENT.CREATE)) {
      const widget = data as Widget
      yield widget
    }
  })
})

export type AppRouter = typeof appRouter

// create server
createHTTPServer({
  middleware: cors(),
  router: appRouter
}).listen(3000)
@samuelgoldenbaum
Copy link
Author

Add a repo here to reproduce

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants
@samuelgoldenbaum and others