How to Fix: Multi-byte characters in Server Action inputs can get mangled when using the Edge Runtime

7 min read

Multi-byte characters break in Next.js Server Actions on the Edge Runtime because the request body is being decoded with the wrong character assumptions.

If your form submits Japanese, emoji, accented text, or any other UTF-8 multi-byte input and your Server Action receives corrupted characters only on the Edge Runtime, you are hitting an encoding boundary where bytes are not being reconstructed correctly before the action reads them.

This issue is subtle because everything may appear fine in local rendering, the browser sends the expected value, and the same action can work in a Node.js runtime. The corruption shows up specifically when the payload is processed in an Edge environment, where request parsing and stream decoding differ from the traditional Node path.

Understanding the Root Cause

The bug happens when form data or a Server Action payload containing multi-byte characters is read from the incoming request stream and decoded in a way that does not preserve the original UTF-8 byte sequence. Characters like é, , or 😀 are not single-byte values. They require multiple bytes, and if those bytes are split, coerced, or reconstructed with the wrong decoder behavior, the result is mojibake—garbled output caused by text being decoded incorrectly.

In practice, this usually occurs in one of these places:

  • The request body is converted to text too early, before the framework correctly parses the action payload.
  • A stream chunk boundary splits a multi-byte character and the decoder does not preserve partial character state correctly.
  • The Edge implementation handling FormData, Request, or action serialization differs from the Node implementation.
  • The framework path assumes text semantics where it should be preserving raw bytes until final UTF-8 decoding.

Why does it seem Edge-specific? Because the Edge Runtime uses Web Standard APIs such as Request, ReadableStream, and TextDecoder rather than Node’s internal request handling. If the framework’s Server Action plumbing has even a small inconsistency in how it buffers or decodes chunks in that environment, only multi-byte characters expose the defect.

So the real root cause is not that Unicode itself is broken. The problem is that bytes are crossing a runtime boundary and being decoded incorrectly before the action receives them.

Step-by-Step Solution

The most reliable fix is to ensure your affected route or action runs on the Node.js runtime until the framework-level Edge bug is fixed upstream. This avoids the problematic Edge decoding path altogether.

1. Confirm the issue is runtime-specific

First, verify that the corruption happens only when using the Edge Runtime. Test with a payload such as Japanese text or emoji.

'use server'

export async function submitMessage(formData: FormData) {
  const message = formData.get('message')
  console.log('received:', message)
}

Submit values like:

  • こんにちは
  • café
  • 😀👍

If the browser sends correct text but the action logs mangled characters only on Edge, you have reproduced the issue.

2. Move the affected segment off Edge

If your page, layout, or route handler explicitly opts into Edge, remove that setting or force Node for the affected segment.

export const runtime = 'nodejs'

Place this in the page, layout, or route segment that owns the Server Action. For example:

// app/contact/page.tsx
export const runtime = 'nodejs'

import { submitMessage } from './actions'

export default function ContactPage() {
  return (
    <form action={submitMessage}>
      <input name="message" />
      <button type="submit">Send</button>
    </form>
  )
}

3. Keep the action in a server-only file

Separate the action so there is no ambiguity about where it executes.

// app/contact/actions.ts
'use server'

export async function submitMessage(formData: FormData) {
  const message = formData.get('message')

  if (typeof message !== 'string') {
    throw new Error('Expected message to be a string')
  }

  console.log('received:', message)
}

This does not itself fix the encoding bug, but it helps isolate the action path and avoids accidental client-side assumptions.

4. Do not manually re-encode or sanitize around the bug

A common mistake is trying to patch the symptom with encodeURIComponent, Base64 wrappers, or custom text transformations in the form layer. Avoid that unless you fully control both ends and must ship a temporary workaround. Most of those approaches add complexity and can break validation, search indexing, logging, or interoperability.

Prefer the runtime change first:

export const runtime = 'nodejs'

5. If you must stay on Edge, use a temporary transport workaround

If moving to Node is impossible, you can bypass the broken action input path by sending a controlled payload format to a route handler and decoding it explicitly. For example, JSON over a route handler may behave more predictably than the affected action serialization path, depending on the framework version.

// app/api/submit/route.ts
export const runtime = 'edge'

export async function POST(request: Request) {
  const data = await request.json()
  const message = data.message

  return Response.json({ received: message })
}
// client component example
async function submitMessage(message) {
  await fetch('/api/submit', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json; charset=utf-8'
    },
    body: JSON.stringify({ message })
  })
}

This is a workaround, not the ideal long-term solution. The clean fix is for the framework to correctly preserve UTF-8 bytes in the Edge Server Action pipeline.

6. Upgrade Next.js when a fix is available

This class of bug is framework-level. Once an upstream patch lands, upgrade and retest your reproduction case using the original multi-byte samples. Track the related implementation and release notes through the project issue and repository rather than relying on assumptions from a workaround.

You can reference the reproduction from the sample repository while validating the fix.

7. Add regression tests for Unicode input

After applying the workaround or upgrade, keep a regression test that covers true multi-byte values.

const samples = [
  'こんにちは',
  'café',
  '😀👍',
  'مرحبا',
  '漢字とかな'
]

for (const value of samples) {
  console.log(value)
}

Your tests should verify exact string equality end-to-end, not just that the request succeeds.

Common Edge Cases

  • Emoji corruption only: Some tests pass with accented Latin characters but fail with emoji because emoji often use more bytes and expose chunking or surrogate handling issues faster.
  • FormData vs JSON differences: A payload may fail through Server Actions but succeed through JSON in a route handler, which points to the action serialization/parsing layer rather than the browser input itself.
  • Mixed runtime segments: If a parent or sibling segment still declares runtime = ‘edge’, the affected action may still execute in a path you did not expect. Audit the route tree carefully.
  • Manual body reads: If custom middleware or handlers read the request body before the framework action parser does, the stream may be consumed or transformed unexpectedly.
  • Normalization confusion: Unicode normalization issues such as NFC vs NFD are different from mojibake. If the characters are readable but compare differently, that is a normalization problem, not this decoding bug.
  • Database false positives: Sometimes the action receives correct text but the database column, driver, or connection encoding stores it incorrectly. Log the raw value at action entry before blaming the runtime.

FAQ

Does this happen because the browser submits the wrong encoding?

Usually no. Modern browsers submit form and JSON payloads as UTF-8 in the common cases relevant here. The corruption is more likely happening during server-side parsing or decoding in the Edge Runtime pipeline.

Why does switching to Node.js fix it?

Because it avoids the specific Edge request-processing path where the multi-byte payload is being mishandled. The Node.js runtime and Edge Runtime do not share identical internals for body streaming and decoding.

Can I safely use Base64 as a permanent workaround?

You can, but it is usually a poor long-term choice. It complicates forms, logging, debugging, payload size, and interoperability. Use it only if you are blocked and cannot move the action to Node.js while waiting for an upstream framework fix.

The practical takeaway is simple: if Server Action inputs become garbled only on the Edge Runtime, treat it as a UTF-8 decoding bug in the request/action pipeline, move the affected path to Node.js, and keep a Unicode regression test in place until the framework release you adopt proves the issue is resolved.

Leave a Reply

Your email address will not be published. Required fields are marked *