Skip to content

Bug: RpcSerialization.msgPack silently fails in environments that block new Function() (e.g. Cloudflare Workers) #6169

@IGassmann

Description

@IGassmann

What version of Effect is running?

@effect/rpc 0.75.0 (with msgpackr ^1.11.4)

What steps can reproduce the bug?

RpcSerialization.msgPack.unsafeMake() creates Packr/Unpackr with default options, which enables msgpackr's JIT record optimization. When decoding arrays of 4+ objects with the same key structure, msgpackr tries to compile a fast reader via new Function(). This is blocked in environments that restrict dynamic code evaluation, such as the Cloudflare Workers runtime.

msgpackr does have a built-in safety mechanism — a startup probe that disables JIT if new Function() is unavailable:

var inlineObjectReadThreshold = 2;

// startup probe — if this fails, JIT is disabled globally
try { new Function('') } catch(error) { inlineObjectReadThreshold = Infinity }

However, this probe gives a false positive on Cloudflare Workers. CF Workers has an allow_eval_during_startup compat flag (default since compatibility_date >= 2025-06-01) that allows new Function() during module initialization but blocks it at runtime. So the probe succeeds, the threshold stays at 2, and later new Function() calls during decode throw. (Reported upstream: kriszyp/msgpackr#179)

How the JIT triggers at runtime:

When packing an array of objects with identical keys, msgpackr encodes the first as a structure definition and subsequent objects as bare record IDs. On decode, it counts reads per structure. After the count exceeds inlineObjectReadThreshold, it calls new Function():

// during decode:
if (readObject.count++ > inlineObjectReadThreshold) {
  // This throws at runtime on CF Workers despite the startup probe succeeding
  structure.read = (new Function('r', 'return function(){return {' + ... + '}}'))(read)
}

This requires 4+ same-structure objects in a single decode call (1st read defines the structure, reads 2-4 increment the count, the 4th read exceeds the threshold of 2).

How the error is silently swallowed:

The relevant code in RpcSerialization.ts:

unsafeMake: () => {
  const unpackr = new Msgpackr.Unpackr()
  const packr = new Msgpackr.Packr()
  // ...
  return {
    decode: (bytes) => {
      // ...
      try {
        return unpackr.unpackMultiple(buf)
      } catch (error_) {
        const error = error_ as any
        if (error.incomplete) {
          incomplete = buf.subarray(error.lastPosition)
          return error.values ?? []
        }
        return []  // ← error lands here, silently returning empty array
      }
    },
    encode: (response) => packr.pack(response)
  }
}

(The silent error swallowing is tracked separately in #6170)

Minimal reproduction:

import { Packr } from "msgpackr"

// This produces a buffer that uses structure records internally
const packed = new Packr().pack([
  { _tag: "Exit", requestId: "1", exit: "ok" },
  { _tag: "Exit", requestId: "2", exit: "ok" },
  { _tag: "Exit", requestId: "3", exit: "ok" },
  { _tag: "Exit", requestId: "4", exit: "ok" }, // 4th object → count > 2 → new Function()
])

// Deploy to CF Workers → unpackMultiple(packed) throws
// RpcSerialization.msgPack.decode() catches it and returns []

Must be deployed to actual CF Workers to reproduce — miniflare does not enforce the new Function() restriction.

What is the expected behavior?

RPC messages should decode correctly in environments that block new Function(). The msgPack serialization should not rely on dynamic code evaluation.

What do you see instead?

The new Function() call throws. The catch block in decode returns an empty array [], silently dropping all RPC messages in the batch. This causes RPC calls to silently fail with no error surfaced to the caller.

Additional information

Possible fixes:

  • Use msgpackr's no-eval entry point — msgpackr ships msgpackr/index-no-eval (since v1.8.0) which strips all new Function() code at the module level. This avoids the JIT entirely while still supporting structure encoding for smaller payloads.

  • Pass { useRecords: false } to Packr/Unpackr — disables the structure/record optimization so new Function() is never reached. Slightly larger payloads but simpler change.

  • Make msgpackr options configurable — expose options on RpcSerialization.msgPack so consumers can pass their own Packr/Unpackr config.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions