mirror of
https://github.com/livekit/livekit.git
synced 2026-09-18 14:25:46 +00:00
Replace the protobuf frame layer on exchange streams with a length-prefixed StreamPreamble followed by opaque HTTP/1.1 bytes. QUIC already provides the multiplexing and per-stream flow control, so the frame layer only re-encoded a message both peers can already parse, and every SDK had to re-materialize HTTP from it. The front serializes a canonical request head from its own parsed *http.Request and never forwards the client's bytes, which is what keeps request smuggling out of the worker. Responses are read with http.ReadResponse, so informational heads need no httptrace hook and the conformance worker loses its HTTP reconstruction entirely. Completion splits by outcome: a body ends by its own framing, a failure after bytes have flowed travels in x-lk-completion / x-lk-error trailers, and a failure before any byte resets the stream with an HttpStreamResetCode. REFUSED stays distinct as the retry-safety signal, and the retry/idempotence rules are unchanged. The x-lk- prefix is reserved for this signalling: it is stripped from client-supplied request headers so a caller cannot forge an outcome, and from responses so it never reaches the end client. Exchange-stream targets are split from the escaped path so a percent-encoded '?' or '/' cannot change which resource the worker routes to. Per-request state moves to attempt.go so the Front and attempt lifecycles stop interleaving; the remaining files are grouped type-first with free helpers last. Those moves are ordering only.
82 lines
2.7 KiB
Go
82 lines
2.7 KiB
Go
// Copyright 2026 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package endpoint_test
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/livekit/livekit-server/pkg/agent/endpoint/wire"
|
|
"github.com/livekit/protocol/livekit"
|
|
)
|
|
|
|
func padHeaders(t *testing.T, url string, bytes int) *http.Request {
|
|
t.Helper()
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
require.NoError(t, err)
|
|
const per = 8 << 10
|
|
for i := 0; i*per < bytes; i++ {
|
|
req.Header.Set(fmt.Sprintf("X-Pad-%d", i), strings.Repeat("p", per))
|
|
}
|
|
return req
|
|
}
|
|
|
|
// A head too large to serialize is answered directly and reaches no worker: no
|
|
// worker could accept it, so spending an attempt on it only turns a permanent
|
|
// failure into a retryable-looking one.
|
|
func TestOversizedHeadIsRejectedWithoutReachingAWorker(t *testing.T) {
|
|
var hits atomic.Int32
|
|
addr := rawTarget(t, func(c net.Conn) {
|
|
hits.Add(1)
|
|
_, _ = io.WriteString(c, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
|
|
})
|
|
base := startFramedWorker(t, addr, []*livekit.AgentHttp_AgentEndpoint{
|
|
{Path: "/h", Methods: []string{"GET"}, Public: true},
|
|
})
|
|
|
|
resp, err := http.DefaultClient.Do(padHeaders(t, base+"/h", wire.MaxRequestHeadSize+(64<<10)))
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode,
|
|
"an unservable head must not come back as a retryable 503")
|
|
require.Empty(t, resp.Header.Get("Retry-After"), "the request can never succeed, so it must not invite a retry")
|
|
require.Zero(t, hits.Load(), "no worker attempt may be spent on a head no worker could accept")
|
|
}
|
|
|
|
// A head comfortably inside the bound is served normally.
|
|
func TestLargeHeadWithinBoundIsServed(t *testing.T) {
|
|
addr := rawTarget(t, func(c net.Conn) {
|
|
_, _ = io.WriteString(c, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
|
|
})
|
|
base := startFramedWorker(t, addr, []*livekit.AgentHttp_AgentEndpoint{
|
|
{Path: "/h", Methods: []string{"GET"}, Public: true},
|
|
})
|
|
|
|
resp, err := http.DefaultClient.Do(padHeaders(t, base+"/h", 512<<10))
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
require.Equal(t, "ok", string(body))
|
|
}
|