Skip to content

Headers are dropped from Request objects if the transfer-encoding header is set #59

@TheNumberOne

Description

@TheNumberOne

What version of Elysia is running?

1.4.28

What version of Node Adapter are you using?

1.4.5

What platform is your computer?

Darwin 25.3.0 arm64 arm

What steps can reproduce the bug?

Run this following script with node script.mjs. It shows that returning the fetch response directly clobbers the content-type header. In my testing, it only happens if the response object has transfer-encoding: chunked as a header.

You can run the script with bun script.mjs to show that it is a bug specific to the NodeJS adapter

import { createServer } from 'node:http';

// Self-contained repro: upstream SSE server + Elysia proxy + verification
// Run with: node repro.mjs   (then separately: bun repro.mjs)
import { Elysia } from 'elysia';

const isBun = typeof globalThis.Bun !== 'undefined';
const runtime = isBun ? 'bun' : 'node';

// Dynamic import of node adapter only when running on Node
const nodeAdapter = isBun ? null : await import('@elysiajs/node');

// 1. Start a raw HTTP upstream that streams SSE (will have transfer-encoding: chunked)
const upstream = createServer((req, res) => {
  res.writeHead(200, {
    'cache-control': 'no-cache',
    'content-type': 'text/event-stream',
  });
  res.write('data: hello\n\n');
  setTimeout(() => {
    res.write('data: world\n\n');
    res.end();
  }, 50);
});

await new Promise((resolve) => upstream.listen(0, resolve));
const upstreamPort = upstream.address().port;

// 2. Elysia proxy — just return fetch() as the docs recommend
const appConfig = isBun ? {} : { adapter: nodeAdapter.node() };
const PROXY_PORT = 19876;
new Elysia(appConfig)
  .get('/proxy', async () => {
    const response = await fetch(`http://127.0.0.1:${upstreamPort}/`);
    console.log(`[${runtime}] response.headers: `, response.headers);
    return response;
  })
  .listen(PROXY_PORT);

// Wait briefly for server to bind
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(`[${runtime}] Upstream on port ${upstreamPort}, proxy on port ${PROXY_PORT}`);

// 3. Fetch through the proxy
const res = await fetch(`http://127.0.0.1:${PROXY_PORT}/proxy`);
const ct = res.headers.get('content-type');
const body = await res.text();

console.log(`[${runtime}] status:       ${res.status}`);
console.log(`[${runtime}] content-type: ${ct}`);
console.log(`[${runtime}] body:         ${JSON.stringify(body)}`);
console.log(
  `\n[${runtime}] ${ct?.includes('text/event-stream') ? 'PASS' : 'FAIL'} — content-type ${ct?.includes('text/event-stream') ? 'preserved' : 'lost (expected text/event-stream, got ' + ct + ')'}`,
);

upstream.close();
process.exit(0);

What is the expected behavior?

The expected output of the script should look like:

[node] Upstream on port 55734, proxy on port 19876
[node] response.headers:  Headers {
  'cache-control': 'no-cache',
  'content-type': 'text/event-stream',
  date: 'Thu, 26 Mar 2026 00:08:36 GMT',
  connection: 'keep-alive',
  'keep-alive': 'timeout=5',
  'transfer-encoding': 'chunked'
}
[node] status:       200
[node] content-type: text/event-stream
[node] body:         "data: hello\n\ndata: world\n\n"

[node] PASS — content-type preserved

What do you see instead?

[node] Upstream on port 55734, proxy on port 19876
[node] response.headers:  Headers {
  'cache-control': 'no-cache',
  'content-type': 'text/event-stream',
  date: 'Thu, 26 Mar 2026 00:08:36 GMT',
  connection: 'keep-alive',
  'keep-alive': 'timeout=5',
  'transfer-encoding': 'chunked'
}
[node] status:       200
[node] content-type: text/plain
[node] body:         "data: hello\n\ndata: world\n\n"

[node] FAIL — content-type lost (expected text/event-stream, got text/plain)

Additional information

A work around is to explicitly unset the transfer-encoding header in the elysia handler. E.g.

new Elysia(appConfig)
  .get('/proxy', async () => {
    const response = await fetch(`http://127.0.0.1:${upstreamPort}/`);
    console.log(`[${runtime}] response.headers: `, response.headers);

    
    const headers = new Headers(upstream.headers);
    headers.delete('transfer-encoding');
  
    return new Response(upstream.body, {
      headers,
      status: upstream.status,
    });
  })
  .listen(PROXY_PORT);

Have you try removing the node_modules and bun.lockb and try again yet?

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions