feat: пересылка клиентских заголовков в апстрим (кроме служебных); Authorization — только ключ провайдера

This commit is contained in:
2026-09-11 03:09:18 +03:00
parent c3aaa914fb
commit 0fb99060f9
2 changed files with 80 additions and 3 deletions
@@ -2,8 +2,9 @@ package pw.binom.llmproxy
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.header
import io.ktor.client.request.headers
import io.ktor.utils.io.readAvailable
import io.ktor.http.Headers
import io.ktor.client.request.preparePost
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
@@ -181,13 +182,24 @@ private suspend fun handleChat(
}
val url = provider.url.trimEnd('/') + "/chat/completions"
val providerKey = resolveEnv(provider.key)
var failover = false
var responded = false
var upstreamStatus = 0
http.preparePost(url) {
header("Authorization", "Bearer ${resolveEnv(provider.key)}")
header("Content-Type", "application/json")
headers {
headersToForward(call.request.headers).forEach { (name, values) ->
appendAll(name, values)
}
// Авторизация — всегда наша (ключ провайдера из конфига);
// клиентский Authorization не пересылается. Если у провайдера
// ключ не задан — Authorization не отправляем вовсе.
if (providerKey.isNotEmpty()) {
set("Authorization", "Bearer $providerKey")
}
set("Content-Type", "application/json")
}
setBody(forwarded.toString())
}.execute { resp ->
upstreamStatus = resp.status.value
@@ -262,6 +274,34 @@ private suspend fun handleChat(
private fun errorJson(msg: String): String =
"""{"error":{"message":"$msg"}}"""
/** Hop-by-hop и прокси-специфичные заголовки, которые НЕ пересылаются в апстрим. */
private val SKIP_HEADER_NAMES = setOf(
"host",
"content-length",
"transfer-encoding",
"te",
"connection",
"proxy-connection",
"keep-alive",
"upgrade",
// Authorization управляется прокси явно (ключ провайдера), клиентский
// не пересылается
"authorization",
)
/**
* Заголовки входящего запроса для пересылки в апстрим: все, кроме служебных
* ([SKIP_HEADER_NAMES]). Тело может быть изменено `patch`, а соединение до
* апстрима — другое, поэтому Content-Length/Transfer-Encoding/Host/Connection
* управляет сам прокси (клиентский Ktor выставит свои автоматически).
* `Authorization` тоже в списке пропусков — прокси управляет им явно
* (ключ провайдера из конфига; клиентский не пересылается).
*/
internal fun headersToForward(request: Headers): Map<String, List<String>> =
request.entries()
.filter { (name, _) -> name.lowercase() !in SKIP_HEADER_NAMES }
.associate { (name, values) -> name to values }
/**
* Сборка тела запроса: подмена `model` на реальное имя апстрима + глубокий
* послойный мерж `patch` в порядке provider → upstream → model.