#21044·ai

Chat transports send duplicate Content-Type values when headers specify it

Author: brennanbutler01Created Sep 17, 2026Updated Sep 18, 2026
Labelstask-identify-issue-type-donetask-bug-reproduction-successfactory-activefactory-automatictask-identify-harness-labels-done

Description

Setting Content-Type in DefaultChatTransport duplicates the default header instead of overriding it. A real HTTP server receives:

application/json, application/json

This happens even when the caller supplies the same application/json value as the default. With application/json; charset=utf-8, the server receives application/json, application/json; charset=utf-8.

Expected: the server receives the configured content type once.

The issue also affects TextStreamChatTransport, per-request headers, and headers returned by prepareSendMessagesRequest. normalizeHeaders() lowercases the supplied key, but HttpChatTransport.sendMessages() adds its default as Content-Type. Both keys survive the object spread and fetch combines their values.

This differs from #9792, which reported dropped headers when using a Headers instance.

Reproduction

In an empty directory, run npm init -y and npm install [email protected]. Save this as repro.mjs and run node repro.mjs. It starts a temporary local HTTP server; no provider or API key is needed.

javascript
import { createServer } from 'node:http';
import { DefaultChatTransport } from 'ai';

const received = [];
const server = createServer((request, response) => {
  received.push(request.headers['content-type']);
  request.resume();
  response.writeHead(200, { 'Content-Type': 'text/event-stream' });
  response.end('data: [DONE]\n\n');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (address === null || typeof address === 'string') throw new Error('Missing local server address.');

try {
  for (const headers of [undefined, { 'Content-Type': 'application/json' }]) {
    const transport = new DefaultChatTransport({
      api: `http://127.0.0.1:${address.port}/chat`,
      headers,
    });
    const stream = await transport.sendMessages({
      chatId: 'synthetic-chat',
      messages: [],
      trigger: 'submit-message',
      messageId: undefined,
    });
    for await (const chunk of stream) { void chunk; }
  }
  console.log(received);
} finally {
  await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
}

Expected:

[ 'application/json', 'application/json' ]

Actual:

[ 'application/json', 'application/json, application/json' ]

Verified over actual local HTTP requests on Node 22 and 24. Ten regression cases per runtime: six fail across the two transports and three header configuration paths; four controls pass for default and unrelated headers.

I used AI assistance to investigate and prepare the reproduction and tests.

AI SDK Version

  • [email protected], Node v24.13.0 and v22.20.0.
  • The affected transport source on main and normalizeHeaders are byte-for-byte identical to the inspected 7.0.101 checkout. Runtime tests used the published package.

Code of Conduct

  • I agree to follow this project's Code of Conduct