Files
livekit/pkg/service/agentendpointmiddleware.go
T
Paul Wells 9ac10fba0e agent endpoints: serve the front on its own middleware chain
The front was mounted on the API mux and served through the API middleware
chain, which is shaped for handlers that buffer a request and write one
response. Four consequences.

negroni.NewRecovery() recovers every panic with no http.ErrAbortHandler
exemption, and NewRecovery sets PrintStack. On an abort it wrote "PANIC: ..."
and a goroutine stack into the already-committed body, then returned normally,
so net/http terminated the chunked stream cleanly. A truncated response
reached the client as complete, with a stack trace appended. Detection was
already correct; delivery was not, and the x-lk- trailers that would otherwise
signal it are stripped before the response leaves. A Content-Length response
still failed safe, since net/http enforces the declared length itself, so the
gap was the chunked and trailer paths.

Endpoints.Disabled documents that it turns the front off, but only
registrations were refused; the mount was unconditional.

The CORS method list omits PUT, which a manifest may declare.

The API body limiter capped request bodies at MaxAPIRequestBodySize, though
the front streams a body through a pooled buffer and never holds one.

NewHTTPHandler now routes the prefix to a chain carrying AgentRecovery, a CORS
list matching the methods a manifest may declare, and the api-key auth
middleware the front resolves a caller's access from. The mount is built only
when endpoints are enabled, so the prefix otherwise falls through and 404s.
RemoveDoubleSlashes moves above the split, so routing and both chains see one
path form.

Taking the front off the mux also stops ServeMux rewriting the paths it is
handed: "//x", "/../" and interior "//" were answered with a redirect rather
than proxied, which a byte-transparent exchange cannot do.

The endpoint stack tests now build the production handler, so they run on the
chain the node serves on.
2026-09-14 13:11:10 -07:00

58 lines
1.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 service
import (
"fmt"
"net/http"
"strings"
"github.com/urfave/negroni/v3"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/agent/endpoint"
)
// IsAgentEndpointPath reports whether an escaped request path targets the agent
// endpoint front. The path must already be normalized; see WithPathNormalization.
func IsAgentEndpointPath(escapedPath string) bool {
return strings.HasPrefix(escapedPath, endpoint.PathPrefix)
}
// AgentRecovery recovers panics, re-panicking http.ErrAbortHandler so it reaches
// net/http and aborts the response.
func AgentRecovery(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
defer func() {
rec := recover()
if rec == nil {
return
}
if rec == http.ErrAbortHandler {
panic(rec)
}
err, ok := rec.(error)
if !ok {
err = fmt.Errorf("%v", rec)
}
logger.Errorw("panic serving agent endpoint", err, "path", r.URL.Path)
// a committed response already has its status on the wire
if nrw, ok := w.(negroni.ResponseWriter); !ok || !nrw.Written() {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next(w, r)
}