Chat transports send duplicate Content-Type values when headers specify it
Description
Setting Content-Type in DefaultChatTransport duplicates the default header instead of overriding it. A real HTTP server receives:
application/json, application/jsonThis 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.
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], Nodev24.13.0andv22.20.0.- The affected transport source on main and
normalizeHeadersare byte-for-byte identical to the inspected7.0.101checkout. Runtime tests used the published package.
Code of Conduct
- I agree to follow this project's Code of Conduct
Source: vercel/ai