continue icon indicating copy to clipboard operation
continue copied to clipboard

IntelliJ: duplicate authorization header sent for OpenAI models, causing authentication failure

Open tim-sh opened this issue 3 months ago • 8 comments
trafficstars

Before submitting your bug report

Relevant environment info

- OS: Linux
- Continue version: 1.0.30
- IDE version: IntelliJ IDEA Community 2025.1
- Model: o4-mini (self-hosted)
- config:
models:
  - name: my o4-mini
    provider: openai
    model: o4-mini
    apiBase: https://acme.com/some/path
    requestOptions:
      headers:
        Authorization: Bearer eyYo…
        my-hdr: bar

Description

When trying to use a self-hosted openai model, the server error response indicates that Continue doesn't send the Authorization header as it should. This does not happen with a self-hosted gemini model, where our server expects the same token and successfully receives it (indicated by absence of error).

Investigating with a proxy, I see this duplication in the raw headers: ['authorization', 'Bearer', 'authorization', 'Bearer [myToken]']. Now the first value, 'Bearer' (token missing), seems to win – at least that's what Node.js' http module makes it into.

Though I can't see the headers received by our server when disabling the proxy, the finding suggests that Continue sends a duplicate Authorization header in my case.

The token has this structure, I would say: [a-zA-Z0-9]{250}\.[a-zA-Z0-9]{6590}\.[a-zA-Z0-9_-]{342}

tim-sh avatar Aug 07 '25 14:08 tim-sh

I can see the same on VS Code with continue version 1.0.21. My openAI compatible server fails authorization because the empty header takes precedence.

atineoSE avatar Aug 11 '25 14:08 atineoSE

Hi, thank you for the detailed ticket! Taking an initial look at this it seems to be a bug, I'm not very familiar with this portion of the code but I'll create a ticket and surface it to the team!

tingwai avatar Aug 14 '25 03:08 tingwai

I'm also having this problem. My case involves using vLLM with basic auth but the problem is essentially the same: no apiKey configured but the provider sends an empty Authorization: Bearer header that interferes with my custom Authorization: Basic <mycredentials> header. I posted a fix proposal as PR https://github.com/continuedev/continue/pull/7803

visadb avatar Sep 17 '25 09:09 visadb

It looks like the fix in #7803 was included today in vscode version 1.2.7-vscode, but I continue to see this issue.

qthequartermasterman avatar Oct 01 '25 20:10 qthequartermasterman

There is the same issue with Ollama provider when used with the CLI. #7803 fix fixes it for OpenAI , so maybe the same fix would work for Ollama . The same configuration works with Codium extention with no problems, but the CLI client sends two Authorization Headers, e regular Basic and an empty Bearer

plknkl avatar Oct 16 '25 10:10 plknkl

It looks like the fix in #7803 was included today in vscode version 1.2.7-vscode, but I continue to see this issue.

Indeed, PR #7803 did not fix the issue for IntelliJ IDEA either, and the double Authorization header kept being sent. Here is a second attempt that I actually had time to test: https://github.com/continuedev/continue/pull/8684

visadb avatar Nov 11 '25 09:11 visadb

Hello folks I tested this out today on a minimal local proxy server. It seemed to work fine.

Only a single authorization headers remains when used on an express server

Output:

OpenAI proxy request headers: {
  accept: "application/json",
  "accept-encoding": "gzip, deflate, br",
  authorization: "my-key-456",
  "content-length": "1901",
  "content-type": "application/json",
  "user-agent": "OpenAI/JS 5.20.3",
  "x-stainless-arch": "arm64",
  "x-stainless-lang": "js",
  "x-stainless-os": "MacOS",
  "x-stainless-package-version": "5.20.3",
  "x-stainless-retry-count": "0",
  "x-stainless-runtime": "node",
  "x-stainless-runtime-version": "v22.20.0",
  host: "127.0.0.1:3000",
  connection: "close",
}

config.yaml


models:
 - name: relay server
    provider: openai
    model: gpt
    apiBase: http://localhost:3000/v1
    requestOptions:
      headers:
        Authorization: "my-key-456"

relay-server.ts

import express, { Request, Response } from "express";

const app = express();
app.use(express.json());

const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";

if (!ANTHROPIC_API_KEY) {
  throw new Error("Missing ANTHROPIC_API_KEY environment variable");
}

type OpenAIChatMessage = {
  role: "system" | "user" | "assistant";
  content: string;
};

type OpenAIChatCompletionRequest = {
  model: string;
  messages: OpenAIChatMessage[];
  max_tokens?: number;
  temperature?: number;
  top_p?: number;
  stream?: boolean;
};

app.post("/v1/chat/completions", async (req: Request, res: Response) => {
  const body = req.body as OpenAIChatCompletionRequest;
  console.log("OpenAI proxy request headers:", req.headers);

  if (!body || !Array.isArray(body.messages)) {
    return res
      .status(400)
      .json({ error: "Invalid request body: missing messages[]" });
  }

  if (body.stream) {
    return res
      .status(400)
      .json({ error: "stream=true is not supported by this minimal proxy" });
  }

  const systemMessages = body.messages.filter((m) => m.role === "system");
  const system = systemMessages.map((m) => m.content).join("\n");

  const nonSystemMessages = body.messages.filter(
    (m) => m.role === "user" || m.role === "assistant"
  );

  const anthropicPayload = {
    model: body.model,
    max_tokens: body.max_tokens ?? 1024,
    temperature: body.temperature ?? 0.7,
    top_p: body.top_p,
    system: system || undefined,
    messages: nonSystemMessages,
  };

  try {
    const response = await fetch(ANTHROPIC_API_URL, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-api-key": ANTHROPIC_API_KEY!,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify(anthropicPayload),
    });

    if (!response.ok) {
      const errorText = await response.text();
      return res
        .status(500)
        .json({ error: "Anthropic API error", details: errorText });
    }

    const data: any = await response.json();
    const text = (data.content ?? [])
      .filter((p: any) => p.type === "text")
      .map((p: any) => p.text)
      .join("");

    const now = Math.floor(Date.now() / 1000);

    const openAIResponse = {
      id: data.id,
      object: "chat.completion",
      created: now,
      model: body.model,
      choices: [
        {
          index: 0,
          message: {
            role: "assistant" as const,
            content: text,
          },
          finish_reason: data.stop_reason === "max_tokens" ? "length" : "stop",
        },
      ],
      usage: {
        prompt_tokens: data.usage?.input_tokens ?? 0,
        completion_tokens: data.usage?.output_tokens ?? 0,
        total_tokens:
          (data.usage?.input_tokens ?? 0) + (data.usage?.output_tokens ?? 0),
      },
    };

    return res.json(openAIResponse);
  } catch (err: any) {
    console.error(err);
    return res
      .status(500)
      .json({ error: "Unexpected error", details: err.message });
  }
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`OpenAI-compatible proxy listening on http://localhost:${port}`);
});

I will need some help with reproduction. Can you help with the config.yaml and an example proxy server that is having the problems?

uinstinct avatar Nov 14 '25 06:11 uinstinct

I will need some help with reproduction.

Hi, and thanks for testing, @uinstinct ! I ran your proxy and it prints only one Authorization header even if multiple are present:

I sent two Authorization headers with curl -X POST http://localhost:3000/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"claude-3-5-sonnet-latest","messages":[{"role":"user","content":"Hello"}],"max_tokens":256}' -H 'Authorization: Bearer foo' -H 'Authorization: Basic bar'

And your proxy code printed out:

OpenAI proxy request headers: {
  host: 'localhost:3000',
  'user-agent': 'curl/7.88.1',
  accept: '*/*',
  'content-type': 'application/json',
  authorization: 'Bearer foo',
  'content-length': '100'
}

And when I listened to port 3000 with netcat instead of your proxy, it printed two Authorization headers for that same curl command:

$ nc -l 3000
POST /v1/chat/completions HTTP/1.1
Host: localhost:3000
User-Agent: curl/7.88.1
Accept: */*
Content-Type: application/json
Authorization: Bearer foo
Authorization: Basic bar
Content-Length: 100

{"model":"claude-3-5-sonnet-latest","messages":[{"role":"user","content":"Hello"}],"max_tokens":256}
Your config.yaml is essentially the same as mine, but here it is in case it helps somehow:
name: Local Agent
version: 1.0.0
schema: v1
models:
  - provider: openai
    name: openai-test
    apiBase: http://localhost:11434
    requestOptions:
      timeout: 60000
      verifySsl: false
      headers:
        Authorization: Basic bG9sOmJhbA==
    model: /models/Intel/Qwen3-30B-A3B-Instruct-2507-int4-AutoRound
    roles:
      - chat
context:
  - provider: code
  - provider: docs
  - provider: diff
  - provider: terminal
  - provider: problems
  - provider: folder
  - provider: codebase
logging:
  level: debug

So could you try netcat instead of your proxy, please? The command is nc -l 3000 for BSD netcat and nc -l -p 3000 for the traditional variety of netcat. At least my IntelliJ IDEA 2025.1 (251.23774.435) with Continue plugin version 1.0.52 still sends two Authorization headers.

visadb avatar Nov 14 '25 07:11 visadb

I see it now! Verified your fix in #8684 and it seems to work. the openai package was adding the Authorization: Bearer header which was causing 2 authorization headers to be sent.

Thanks for the nc -l 3000! This can be handy for testing

uinstinct avatar Nov 18 '25 11:11 uinstinct

Appreciate the insights all! Merged @visadb's fix

RomneyDa avatar Nov 18 '25 18:11 RomneyDa

:tada: This issue has been resolved in version 1.31.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

sestinj avatar Nov 19 '25 22:11 sestinj