Skip to content

fix(server-elysia): Add Node.js compatibility and VoltOps Console support#993

Merged
omeraplak merged 3 commits into
VoltAgent:mainfrom
Artist-MOBAI:fix/server-elysia-nodejs-compatibility
Jan 27, 2026
Merged

fix(server-elysia): Add Node.js compatibility and VoltOps Console support#993
omeraplak merged 3 commits into
VoltAgent:mainfrom
Artist-MOBAI:fix/server-elysia-nodejs-compatibility

Conversation

@Artist-MOBAI

@Artist-MOBAI Artist-MOBAI commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

Problem

When using @voltagent/server-elysia and connecting to VoltOps Console, the dashboard keeps spinning indefinitely and never loads, while @voltagent/server-hono works correctly with the exact same project setup.

Screenshot 2026-01-27 at 19 48 09

Reproduction Steps

  1. Create a new VoltAgent project using @voltagent/server-elysia
  2. Start the server with npm run dev (using Node.js runtime, not Bun)
  3. Open VoltOps Console
  4. Enter the server URL (e.g., http://localhost:3141) and attempt to connect
  5. Expected: Console displays agents and workflows immediately (as it does with @voltagent/server-hono)
  6. Actual: Console shows a loading spinner that never completes

Observed Symptoms

After investigation, the following issues were observed:

  1. Infinite Loading Spinner

    • VoltOps Console stays in loading state indefinitely
    • The connection never establishes successfully
  2. "Local Connection Blocked by Browser" Error

    • After the loading timeout, Console displays this error message
    • Indicates that the browser is blocking requests to the local server
  3. CORS Errors in Browser Console

    • Multiple CORS errors appear for API endpoints:
      Access to fetch at 'http://localhost:3141/agents' from origin 'https://console.voltagent.dev' 
      has been blocked by CORS policy
      
    • Affected endpoints: /agents, /workflows, /tools, /observability/health, etc.
  4. API Requests Not Responding (when tested with curl)

    • Direct API calls to the server timeout:
      $ curl http://localhost:3141/agents
      # No response, exits with code 28 (connection timeout)
    • This indicates the server isn't properly handling requests in Node.js environment

Environment

  • Runtime: Node.js
  • Server Package: @voltagent/server-elysia
  • Working Comparison: @voltagent/server-hono works correctly in the same environment

Comparison with server-hono

To verify this is an server-elysia specific issue, I created two identical projects:

Setup Package Runtime VoltOps Console
Project A @voltagent/server-hono Node.js Works perfectly
Project B @voltagent/server-elysia Node.js Infinite loading

Both projects have identical agent configurations, tools, and workflows. The only difference is the server package used.


Root Cause Analysis

After comparing @voltagent/server-elysia with @voltagent/server-hono, three key issues were identified:

1. Missing @voltagent/resumable-streams Integration

server-hono integrates resolveResumableStreamDeps to enable VoltOps Console connectivity:

// server-hono/src/app-factory.ts
import { resolveResumableStreamDeps } from "@voltagent/resumable-streams";

const baseDeps = await resolveResumableStreamDeps(deps, resumableStreamConfig?.adapter, logger);

server-elysia was missing this integration entirely, causing Console to fail to establish connection with the local server.

2. Elysia's app.listen() Doesn't Work in Node.js

Elysia is designed primarily for Bun runtime. The app.listen() method relies on Bun's native server implementation and does not function correctly in Node.js environment:

// Original code - works in Bun, fails in Node.js
const server = app.listen({ port, hostname });

server-hono uses @hono/node-server's serve() function which properly bridges Hono's fetch API to Node.js HTTP server. Since there's no equivalent @elysia/node-server package, manual bridging is required.

3. Incompatible CORS Default Values

Elysia's @elysiajs/cors has different default values compared to Hono's cors middleware:

Setting Hono Default Elysia Default Issue
credentials false true Browser blocks credentials: true with origin: "*"
allowedHeaders Dynamic (reflects request) Fixed list Custom headers like x-voltagent-dev are rejected

Changes

1. package.json

Added @voltagent/resumable-streams dependency:

"@voltagent/resumable-streams": "^2.0.1"

2. src/app-factory.ts

Added resumable-streams integration:

import { resolveResumableStreamDeps } from "@voltagent/resumable-streams";

// Resolve resumable stream dependencies (like server-hono does)
const resumableStreamConfig = config.resumableStream;
const baseDeps = await resolveResumableStreamDeps(deps, resumableStreamConfig?.adapter, logger);
const resumableStreamDefault =
  typeof resumableStreamConfig?.defaultEnabled === "boolean"
    ? resumableStreamConfig.defaultEnabled
    : baseDeps.resumableStreamDefault;
const resolvedDeps: ServerProviderDeps = {
  ...baseDeps,
  ...(resumableStreamDefault !== undefined ? { resumableStreamDefault } : {}),
};

Fixed CORS configuration with sensible defaults:

cors: () => {
  if (config.cors !== false) {
    app.use(
      cors({
        origin: config.cors?.origin ?? "*",
        methods: config.cors?.allowMethods ?? ["GET", "POST", "PUT", "DELETE", "PATCH"],
        allowedHeaders: config.cors?.allowHeaders ?? ["Content-Type", "Authorization"],
        credentials: config.cors?.credentials ?? false,
        maxAge: config.cors?.maxAge ?? 86400,
        exposeHeaders: config.cors?.exposeHeaders,
      } as Parameters<typeof cors>[0]),
    );
  }
},

3. src/elysia-server-provider.ts

Replaced app.listen() with Node.js HTTP server:

protected async startServer(port: number): Promise<Server> {
  const { app } = await createApp(this.deps, this.elysiaConfig, port);
  this.app = app;

  // Elysia's app.listen() is designed for Bun runtime and doesn't work properly in Node.js
  const { createServer } = await import("node:http");
  const server = createServer(async (req, res) => {
    try {
      const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
      const headers = new Headers();
      for (const [key, value] of Object.entries(req.headers)) {
        if (value) {
          headers.set(key, Array.isArray(value) ? value.join(", ") : value);
        }
      }
      const body = req.method !== "GET" && req.method !== "HEAD" ? req : undefined;
      const request = new Request(url.toString(), {
        method: req.method,
        headers,
        body,
        duplex: "half",
      } as RequestInit);
      const response = await app.fetch(request);
      res.statusCode = response.status;
      response.headers.forEach((value, key) => {
        res.setHeader(key, value);
      });
      if (response.body) {
        const reader = response.body.getReader();
        const pump = async (): Promise<void> => {
          const { done, value } = await reader.read();
          if (done) {
            res.end();
            return;
          }
          res.write(value);
          await pump();
        };
        await pump();
      } else {
        res.end();
      }
    } catch (error) {
      console.error("Request error:", error);
      res.statusCode = 500;
      res.end("Internal Server Error");
    }
  });

  return new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(port, this.elysiaConfig.hostname || "0.0.0.0", () => {
      resolve(server);
    });
  });
}

Summary

Issue Cause Fix
Console keeps spinning Missing @voltagent/resumable-streams integration Added resolveResumableStreamDeps call
API requests timeout app.listen() doesn't work in Node.js Manual Node.js HTTP server bridge
CORS errors Elysia's cors defaults incompatible with browser Set sensible defaults (credentials: false)

Summary by cubic

Fixes VoltOps Console connectivity and Node.js compatibility in @voltagent/server-elysia. Console now loads correctly and API endpoints respond when running under Node.js.

  • Bug Fixes

    • Replaced Bun-only app.listen with a Node.js HTTP bridge that forwards requests to app.fetch.
    • Integrated resolveResumableStreamDeps to enable resumable streams and Console connection.
    • Set safe CORS defaults to prevent browser blocking (credentials: false, permissive origin/headers).
  • Dependencies

    • Added @voltagent/resumable-streams (^2.0.1).

Written for commit 81f024e. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added resumable streams support with a configurable adapter and optional default enablement.
  • Improvements

    • Switched server runtime to a pure Node.js HTTP server for broader compatibility.
    • Streamlined CORS handling with sensible defaults and exposed-headers support.
  • Chores

    • Added a dependency and included a patch changeset for release.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot

changeset-bot Bot commented Jan 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 81f024e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/server-elysia Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jan 27, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Added optional resumable-stream configuration and dependency; migrated server startup from Bun-specific listen to a Node.js http.createServer proxying requests to the Elysia app via app.fetch; propagated resolved resumable-stream deps into route registration and simplified CORS setup.

Changes

Cohort / File(s) Summary
Package & Types
packages/server-elysia/package.json, packages/server-elysia/src/types.ts
Added @voltagent/resumable-streams dependency and new resumableStream config on ElysiaServerConfig (adapter + optional defaultEnabled).
App Factory & Routing
packages/server-elysia/src/app-factory.ts
Resolve resumable-stream dependencies, derive resumableStreamDefault, merge into resolved deps, register routes with resolvedDeps, and replace manual CORS assembly with a streamlined options object (nullish coalescing defaults; exposeHeaders added).
Server Provider (Node HTTP)
packages/server-elysia/src/elysia-server-provider.ts
Replaced Bun/WebStandard app.listen() flow with node:http createServer; construct a Web Request from incoming Node req, call app.fetch, map headers/status, stream response body to Node res, and add request error handling.
Release Metadata
.changeset/wicked-worms-wash.md
Added changeset bump (patch) for @voltagent/server-elysia noting Node.js compatibility and VoltOps Console support.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NodeHTTP as Node HTTP Server
    participant Elysia as Elysia App
    participant ResStream as Resumable Stream Adapter

    Client->>NodeHTTP: TCP HTTP request
    NodeHTTP->>NodeHTTP: Construct Web Request from Node req
    NodeHTTP->>Elysia: app.fetch(request)
    Elysia->>ResStream: Check resumableStream adapter/config
    ResStream-->>Elysia: Provide stream handling behavior
    Elysia-->>NodeHTTP: Response (headers, status, body stream)
    NodeHTTP->>NodeHTTP: Map headers/status, pipe body to res
    NodeHTTP->>Client: Stream response and finish
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped from Bun to Node today,

Streams that pause now find their way,
Requests proxied, headers bright,
CORS trimmed clean in morning light,
A little hop — the server's play ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main changes: fixing Node.js compatibility and enabling VoltOps Console support in server-elysia, which directly aligns with the core objectives and changes throughout the changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering problem statement, reproduction steps, root cause analysis, and detailed changes. It exceeds template requirements with clear context and thorough explanations, though the template sections are not explicitly followed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/server-elysia/src/app-factory.ts`:
- Around line 66-67: Remove the unnecessary `as any` on the `a2a` entry: pass
`resolvedDeps` directly to registerA2ARoutes because it already matches
ServerProviderDeps; for the `mcp` entry replace the unsafe `as any` with a
precise cast that exposes only the mcp piece (e.g. cast to McpDeps or use
Pick<typeof resolvedDeps, 'mcp'>) so registerMcpRoutes receives the expected
McpDeps while preserving type safety for resolvedDeps, referencing
registerA2ARoutes, registerMcpRoutes, resolvedDeps, McpDeps and
ServerProviderDeps.
🧹 Nitpick comments (2)
packages/server-elysia/src/elysia-server-provider.ts (2)

80-84: Replace console.error with the class logger.

The ElysiaServerProvider class has access to this.logger from the base class. Using console.error bypasses the structured logging infrastructure.

Proposed fix
       } catch (error) {
-        console.error("Request error:", error);
+        this.logger.error("Request error:", { error });
         res.statusCode = 500;
         res.end("Internal Server Error");
       }

65-78: Consider iterative streaming with backpressure handling.

The recursive pump() function has two concerns:

  1. Stack depth: For responses with many chunks, deep recursion could risk stack overflow (though V8 typically handles tail-call-like patterns well).
  2. Backpressure: res.write() returns false when the internal buffer is full, signaling the caller should wait for the 'drain' event before writing more data. Ignoring this can cause memory pressure under load.
Iterative approach with backpressure
         if (response.body) {
           const reader = response.body.getReader();
-          const pump = async (): Promise<void> => {
-            const { done, value } = await reader.read();
-            if (done) {
-              res.end();
-              return;
-            }
-            res.write(value);
-            await pump();
-          };
-          await pump();
+          while (true) {
+            const { done, value } = await reader.read();
+            if (done) {
+              res.end();
+              break;
+            }
+            const canContinue = res.write(value);
+            if (!canContinue) {
+              await new Promise<void>((resolve) => res.once("drain", resolve));
+            }
+          }
         } else {
           res.end();
         }

Comment thread packages/server-elysia/src/app-factory.ts Outdated

@omeraplak omeraplak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Artist-MOBAI ,
Thank you so much 🥳

@omeraplak
omeraplak merged commit 48cc93f into VoltAgent:main Jan 27, 2026
5 of 22 checks passed
@Artist-MOBAI

Copy link
Copy Markdown
Contributor Author

Hey @Artist-MOBAI , Thank you so much 🥳

Hey Omer, thanks a ton! Great to meet you. I’m part of @AdventureX-RGE (China’s largest hackathon). I’ll be in the US in a few months and would love to grab a coffee and chat if you’re around!

@omeraplak

Copy link
Copy Markdown
Member

Hey @Artist-MOBAI , Thank you so much 🥳

Hey Omer, thanks a ton! Great to meet you. I’m part of @AdventureX-RGE (China’s largest hackathon). I’ll be in the US in a few months and would love to grab a coffee and chat if you’re around!

Hey, that’s awesome! I’m almost always in San Francisco. When you’re here, feel free to message me here or on Discord. I’d be happy to grab a coffee together. ☕️

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

Successfully merging this pull request may close these issues.

2 participants