Files
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

645 lines
25 KiB
Go

// Package lib provides core functionality for the Bifrost HTTP service,
// including context propagation, header management, and integration with monitoring systems.
//
// This package handles the conversion of FastHTTP request contexts to Bifrost contexts,
// ensuring that important metadata and tracking information is preserved across the system.
// It supports propagation of both Prometheus metrics and Maxim tracing data through HTTP headers.
package lib
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/plugins/governance"
"github.com/maximhq/bifrost/plugins/maxim"
"github.com/maximhq/bifrost/plugins/semanticcache"
"github.com/valyala/fasthttp"
)
const (
// FastHTTPUserValueBifrostContext stores the active *schemas.BifrostContext on fasthttp.RequestCtx.
// This allows transport middleware and request handlers to share the same context instance.
FastHTTPUserValueBifrostContext = "__bifrost_context"
// FastHTTPUserValueBifrostCancel stores the cancel func for the active shared Bifrost context.
FastHTTPUserValueBifrostCancel = "__bifrost_context_cancel"
// FastHTTPUserValueLargeResponseMode marks requests that streamed a large response body.
// It is used by transport middleware to avoid re-buffering response bodies for post-hooks.
FastHTTPUserValueLargeResponseMode = "__bifrost_large_response_mode"
)
// ParseSessionIDFromBaggage extracts the session-id baggage member value.
// It supports simple W3C baggage parsing sufficient for log grouping.
func ParseSessionIDFromBaggage(header string) string {
for _, member := range strings.Split(header, ",") {
member = strings.TrimSpace(member)
if member == "" {
continue
}
parts := strings.SplitN(member, ";", 2)
kv := strings.SplitN(strings.TrimSpace(parts[0]), "=", 2)
if len(kv) != 2 {
continue
}
key := strings.ToLower(strings.TrimSpace(kv[0]))
value := strings.TrimSpace(kv[1])
if key != "session-id" || value == "" {
continue
}
if len(value) > 255 {
if logger != nil {
logger.Warn("session-id exceeds 255 chars, ignoring: length=%d, prefix=%s", len(value), value[:255])
}
continue
}
return value
}
return ""
}
// ConvertToBifrostContext converts a FastHTTP RequestCtx to a Bifrost context,
// preserving important header values for monitoring and tracing purposes.
//
// The function processes several types of special headers:
// 1. Prometheus Headers (x-bf-prom-*):
// - All headers prefixed with 'x-bf-prom-' are copied to the context
// - The prefix is stripped and the remainder becomes the context key
// - Example: 'x-bf-prom-latency' becomes 'latency' in the context
//
// 2. Maxim Tracing Headers (x-bf-maxim-*):
// - Specifically handles 'x-bf-maxim-traceID' and 'x-bf-maxim-generationID'
// - These headers enable trace correlation across service boundaries
// - Values are stored using Maxim's context keys for consistency
//
// 3. MCP Headers (x-bf-mcp-*):
// - Specifically handles 'x-bf-mcp-include-clients' and 'x-bf-mcp-include-tools' (include-only filtering)
// - These headers enable MCP client and tool filtering
// - Values are stored using MCP context keys for consistency
//
// 4. Governance Headers:
// - x-bf-vk: Virtual key for governance (required for governance to work)
//
// 5. API Key Headers:
// - Authorization: Bearer token format only (e.g., "Bearer sk-...") - OpenAI style
// - x-api-key: Direct API key value - Anthropic style
// - x-goog-api-key: Direct API key value - Google Gemini style
// - x-bf-api-key references a stored API key name rather than the raw secret.
// - Keys are extracted and stored in the context using schemas.BifrostContextKey
// - This enables explicit key usage for requests via headers
//
// 6. Cancellable Context:
// - Creates a cancellable context that can be used to cancel upstream requests when clients disconnect
// - This is critical for streaming requests where write errors indicate client disconnects
// - Also useful for non-streaming requests to allow provider-level cancellation
//
// 7. Extra Headers (x-bf-eh-*):
// - Any header starting with 'x-bf-eh-' is collected and added to the map stored under schemas.BifrostContextKeyExtraHeaders
// - The prefix is stripped, the remainder is lower-cased, and duplicate names append values
// - This allows callers to send arbitrary context metadata without needing to extend the public schema
//
// 8. Session Stickiness Headers:
// - x-bf-session-id: Session identifier for key binding (reuse same key across requests)
// - x-bf-session-ttl: Per-request TTL override (duration string e.g. "30m" or seconds integer)
//
// 9. Raw Capture Headers (per-request override of provider config; accepts "true" or "false"):
// - x-bf-send-back-raw-request: include raw provider request in the BifrostResponse returned to the caller
// - x-bf-send-back-raw-response: include raw provider response in the BifrostResponse returned to the caller
// - x-bf-store-raw-request-response: capture raw request/response for logging only (stripped from client response)
// Parameters:
// - ctx: The FastHTTP request context containing the original headers
// - allowDirectKeys: Whether to allow direct API key usage from headers
//
// Returns:
// - *context.Context: A new cancellable context.Context containing the propagated values
// - context.CancelFunc: Function to cancel the context (should be called when request completes)
//
// Example Usage:
//
// fastCtx := &fasthttp.RequestCtx{...}
// bifrostCtx, cancel := ConvertToBifrostContext(fastCtx, true, nil)
// defer cancel() // Ensure cleanup
// // bifrostCtx now contains propagated header values including Prometheus metrics,
// // Maxim tracing data, MCP filters, governance keys, API keys, cache settings,
// // session stickiness, and extra headers
func ConvertToBifrostContext(ctx *fasthttp.RequestCtx, allowDirectKeys bool, matcher *HeaderMatcher, mcpHeaderCombinedAllowlist schemas.WhiteList) (*schemas.BifrostContext, context.CancelFunc) {
// Reuse a shared request-scoped context when available.
var bifrostCtx *schemas.BifrostContext
var cancel context.CancelFunc
if existing, ok := ctx.UserValue(FastHTTPUserValueBifrostContext).(*schemas.BifrostContext); ok && existing != nil {
if existingCancel, ok := ctx.UserValue(FastHTTPUserValueBifrostCancel).(context.CancelFunc); ok && existingCancel != nil {
bifrostCtx = existing
cancel = existingCancel
} else {
// Create one cancellable child context and promote it as the shared context.
bifrostCtx, cancel = schemas.NewBifrostContextWithCancel(existing)
ctx.SetUserValue(FastHTTPUserValueBifrostContext, bifrostCtx)
ctx.SetUserValue(FastHTTPUserValueBifrostCancel, cancel)
}
}
if bifrostCtx == nil {
// Create cancellable context for requests that don't have a shared context yet.
parent := context.Context(ctx)
func() {
// Zero-value fasthttp.RequestCtx can panic on Done(); fall back safely.
defer func() {
if recover() != nil {
parent = context.Background()
}
}()
_ = ctx.Done()
}()
bifrostCtx, cancel = schemas.NewBifrostContextWithCancel(parent)
ctx.SetUserValue(FastHTTPUserValueBifrostContext, bifrostCtx)
ctx.SetUserValue(FastHTTPUserValueBifrostCancel, cancel)
}
// Preserve existing request-id if already present on the shared context.
if existingRequestID, ok := bifrostCtx.Value(schemas.BifrostContextKeyRequestID).(string); !ok || existingRequestID == "" {
// First, check if x-request-id header exists
requestID := string(ctx.Request.Header.Peek("x-request-id"))
if requestID == "" {
requestID = uuid.New().String()
}
bifrostCtx.SetValue(schemas.BifrostContextKeyRequestID, requestID)
}
// Populating all user values from the request context
ctx.VisitUserValuesAll(func(key, value any) {
bifrostCtx.SetValue(key, value)
})
// Initialize tags map for collecting maxim tags
maximTags := make(map[string]string)
// Initialize extra headers map for headers prefixed with x-bf-eh-
extraHeaders := make(map[string][]string)
// Initialize extra headers map for headers in the mcp header combined allowlist
mcpExtraHeaders := make(map[string][]string)
// Security denylist of header names that should never be accepted (case-insensitive)
// This denylist is always enforced regardless of user configuration
securityDenylist := map[string]bool{
"proxy-authorization": true,
"cookie": true,
"host": true,
"content-length": true,
"connection": true,
"transfer-encoding": true,
// prevent auth/key overrides via x-bf-eh-*
"x-api-key": true,
"x-goog-api-key": true,
"x-bf-api-key": true,
"x-bf-api-key-id": true,
"x-bf-vk": true,
}
// Debug: Log header matcher state
if logger != nil {
if matcher != nil {
logger.Debug("headerMatcher hasAllowlist=%v, hasDenylist=%v", matcher.HasAllowlist(), matcher.hasDenylist)
} else {
logger.Debug("headerMatcher is nil (allow all)")
}
}
// Then process other headers
ctx.Request.Header.All()(func(key, value []byte) bool {
keyStr := strings.ToLower(string(key))
if keyStr == "baggage" {
if sessionID := ParseSessionIDFromBaggage(string(value)); sessionID != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeyParentRequestID, sessionID)
}
return true
}
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-prom-"); ok {
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
return true
}
// Checking for maxim headers
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-maxim-"); ok {
switch labelName {
case string(maxim.GenerationIDKey):
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
case string(maxim.TraceIDKey):
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
case string(maxim.SessionIDKey):
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
case string(maxim.TraceNameKey):
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
case string(maxim.GenerationNameKey):
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
case string(maxim.LogRepoIDKey):
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
default:
// apart from these all headers starting with x-bf-maxim- are keys for tags
// collect them in the maximTags map
maximTags[labelName] = string(value)
}
return true
}
// MCP control headers (include-only filtering)
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-mcp-"); ok {
switch labelName {
case "include-clients":
fallthrough
case "include-tools":
// Parse comma-separated values into []string
valueStr := string(value)
var parsedValues []string
if valueStr != "" {
// Split by comma and trim whitespace
for _, v := range strings.Split(valueStr, ",") {
if trimmed := strings.TrimSpace(v); trimmed != "" {
parsedValues = append(parsedValues, trimmed)
}
}
} else {
parsedValues = []string{""}
}
bifrostCtx.SetValue(schemas.BifrostContextKey("mcp-"+labelName), parsedValues)
return true
}
}
// Handle virtual key header (x-bf-vk, authorization, x-api-key, x-goog-api-key headers)
if keyStr == string(schemas.BifrostContextKeyVirtualKey) {
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, string(value))
return true
}
if keyStr == "authorization" {
valueStr := string(value)
// Only accept Bearer token format: "Bearer ..."
if strings.HasPrefix(strings.ToLower(valueStr), "bearer ") {
authHeaderValue := strings.TrimSpace(valueStr[7:]) // Remove "Bearer " prefix
if authHeaderValue != "" && strings.HasPrefix(strings.ToLower(authHeaderValue), governance.VirtualKeyPrefix) {
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, authHeaderValue)
return true
}
}
}
if keyStr == "x-api-key" && strings.HasPrefix(strings.ToLower(string(value)), governance.VirtualKeyPrefix) {
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, string(value))
return true
}
if keyStr == "x-goog-api-key" && strings.HasPrefix(strings.ToLower(string(value)), governance.VirtualKeyPrefix) {
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, string(value))
return true
}
if keyStr == "x-bf-api-key" {
if keyName := strings.TrimSpace(string(value)); keyName != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeyAPIKeyName, keyName)
}
return true
}
if keyStr == "x-bf-api-key-id" {
if keyID := strings.TrimSpace(string(value)); keyID != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeyAPIKeyID, keyID)
}
return true
}
// Handle cache key header (x-bf-cache-key)
if keyStr == "x-bf-cache-key" {
bifrostCtx.SetValue(semanticcache.CacheKey, string(value))
return true
}
// Handle cache TTL header (x-bf-cache-ttl)
if keyStr == "x-bf-cache-ttl" {
valueStr := string(value)
var ttlDuration time.Duration
var err error
// First try to parse as duration (e.g., "30s", "5m", "1h")
if ttlDuration, err = time.ParseDuration(valueStr); err != nil {
// If that fails, try to parse as plain number and treat as seconds
if seconds, parseErr := strconv.Atoi(valueStr); parseErr == nil && seconds > 0 {
ttlDuration = time.Duration(seconds) * time.Second
err = nil // Reset error since we successfully parsed as seconds
}
}
if err == nil {
bifrostCtx.SetValue(semanticcache.CacheTTLKey, ttlDuration)
}
// If both parsing attempts fail, we silently ignore the header and use default TTL
return true
}
// Cache threshold header
if keyStr == "x-bf-cache-threshold" {
threshold, err := strconv.ParseFloat(string(value), 64)
if err == nil {
// Clamp threshold to the inclusive range [0.0, 1.0]
if threshold < 0.0 {
threshold = 0.0
} else if threshold > 1.0 {
threshold = 1.0
}
bifrostCtx.SetValue(semanticcache.CacheThresholdKey, threshold)
}
// If parsing fails, silently ignore the header (no context value set)
return true
}
// Cache type header
if keyStr == "x-bf-cache-type" {
bifrostCtx.SetValue(semanticcache.CacheTypeKey, semanticcache.CacheType(string(value)))
return true
}
// Cache no store header
if keyStr == "x-bf-cache-no-store" {
if valueStr := string(value); valueStr == "true" {
bifrostCtx.SetValue(semanticcache.CacheNoStoreKey, true)
}
return true
}
// Session stickiness: session ID for key binding
if keyStr == "x-bf-session-id" {
if valueStr := strings.TrimSpace(string(value)); valueStr != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeySessionID, valueStr)
}
return true
}
// Session stickiness: per-request TTL override (duration string or seconds integer)
if keyStr == "x-bf-session-ttl" {
valueStr := strings.TrimSpace(string(value))
var ttlDuration time.Duration
var err error
if ttlDuration, err = time.ParseDuration(valueStr); err != nil {
if seconds, parseErr := strconv.Atoi(valueStr); parseErr == nil && seconds > 0 {
ttlDuration = time.Duration(seconds) * time.Second
err = nil
}
}
if err == nil && ttlDuration > 0 {
bifrostCtx.SetValue(schemas.BifrostContextKeySessionTTL, ttlDuration)
}
return true
}
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-eh-"); ok {
// Skip empty header names after prefix removal
if labelName == "" {
return true
}
// Normalize header name to lowercase
labelName = strings.ToLower(labelName)
// Validate against security denylist (always enforced)
if securityDenylist[labelName] {
return true
}
// Apply configurable header filter
if !matcher.ShouldAllow(labelName) {
return true
}
// Append header value (allow multiple values for the same header)
extraHeaders[labelName] = append(extraHeaders[labelName], string(value))
return true
}
// Direct header forwarding: when allowlist is configured, any header explicitly
// in the allowlist can be forwarded directly without the x-bf-eh- prefix.
// This enables forwarding arbitrary headers like "anthropic-beta" directly.
// Only applies when allowlist is non-empty (backward compatible).
if matcher.HasAllowlist() {
if matcher.MatchesAllow(keyStr) {
// Skip reserved x-bf-* headers (handled separately)
if strings.HasPrefix(keyStr, "x-bf-") {
return true
}
// Validate against security denylist (always enforced)
if securityDenylist[keyStr] {
return true
}
// Check denylist
if matcher.MatchesDeny(keyStr) {
return true
}
// Forward the header directly with its original name
if logger != nil {
logger.Debug("forwarding header via allowlist: %s", keyStr)
}
extraHeaders[keyStr] = append(extraHeaders[keyStr], string(value))
return true
}
}
// Handle MCP extra headers
if mcpHeaderCombinedAllowlist.IsAllowed(keyStr) {
mcpExtraHeaders[keyStr] = append(mcpExtraHeaders[keyStr], string(value))
return true
}
// Raw capture headers — all three support "true"/"false" to fully override the
// provider-level config for this request.
if keyStr == "x-bf-send-back-raw-request" {
if b, err := strconv.ParseBool(string(value)); err == nil {
bifrostCtx.SetValue(schemas.BifrostContextKeySendBackRawRequest, b)
}
return true
}
if keyStr == "x-bf-send-back-raw-response" {
if b, err := strconv.ParseBool(string(value)); err == nil {
bifrostCtx.SetValue(schemas.BifrostContextKeySendBackRawResponse, b)
}
return true
}
if keyStr == "x-bf-store-raw-request-response" {
if b, err := strconv.ParseBool(string(value)); err == nil {
bifrostCtx.SetValue(schemas.BifrostContextKeyStoreRawRequestResponse, b)
}
return true
}
// Parent request ID header (for linking MCP tool calls to parent LLM requests)
if keyStr == "x-bf-parent-request-id" {
if valueStr := strings.TrimSpace(string(value)); valueStr != "" {
bifrostCtx.SetValue(schemas.BifrostMCPAgentOriginalRequestID, valueStr)
}
return true
}
// Add passthrough extra params header support
if keyStr == "x-bf-passthrough-extra-params" {
if valueStr := string(value); valueStr == "true" {
bifrostCtx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
}
return true
}
// Compat header: per-request override of compat plugin settings.
// Accepts: "true" (enable all), JSON array of feature names, or ["*"] (enable all).
// An empty array [] or absent header means no overrides.
if keyStr == "x-bf-compat" {
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatConvertTextToChat)
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatConvertChatToResponses)
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatShouldDropParams)
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatShouldConvertParams)
valueStr := strings.TrimSpace(string(value))
if valueStr == "true" {
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertTextToChat, true)
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertChatToResponses, true)
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldDropParams, true)
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldConvertParams, true)
} else if strings.HasPrefix(valueStr, "[") {
var features []string
if err := json.Unmarshal([]byte(valueStr), &features); err == nil {
if len(features) == 1 && features[0] == "*" {
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertTextToChat, true)
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertChatToResponses, true)
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldDropParams, true)
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldConvertParams, true)
} else {
for _, f := range features {
switch f {
case "convert_text_to_chat":
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertTextToChat, true)
case "convert_chat_to_responses":
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertChatToResponses, true)
case "should_drop_params":
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldDropParams, true)
case "should_convert_params":
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldConvertParams, true)
}
}
}
}
}
return true
}
return true
})
// Store the collected maxim tags in the context
if len(maximTags) > 0 {
bifrostCtx.SetValue(schemas.BifrostContextKey(maxim.TagsKey), maximTags)
}
// Store collected extra headers in the context if any were found
if len(extraHeaders) > 0 {
bifrostCtx.SetValue(schemas.BifrostContextKeyExtraHeaders, extraHeaders)
}
// Store collected MCP extra headers in the context if any were found
if len(mcpExtraHeaders) > 0 {
bifrostCtx.SetValue(schemas.BifrostContextKeyMCPExtraHeaders, mcpExtraHeaders)
}
// Collect all request headers for downstream use (e.g., governance required headers check)
// Keys are lowercased for case-insensitive lookup
allHeaders := make(map[string]string)
ctx.Request.Header.All()(func(key, value []byte) bool {
allHeaders[strings.ToLower(string(key))] = string(value)
return true
})
bifrostCtx.SetValue(schemas.BifrostContextKeyRequestHeaders, allHeaders)
// Extract per-user MCP OAuth user identifier from X-Bf-User-Id header
if mcpUserID := string(ctx.Request.Header.Peek("X-Bf-User-Id")); mcpUserID != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeyMCPUserID, mcpUserID)
}
// Build and set OAuth redirect URI for per-user OAuth flows
scheme := "http"
if ctx.IsTLS() || string(ctx.Request.Header.Peek("X-Forwarded-Proto")) == "https" {
scheme = "https"
}
host := string(ctx.Host())
if host != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeyOAuthRedirectURI, fmt.Sprintf("%s://%s/api/oauth/callback", scheme, host))
}
if allowDirectKeys {
// Extract API key from Authorization header (Bearer format), x-api-key, or x-goog-api-key header
var apiKey string
// TODO: fix plugin data leak
// Check Authorization header (Bearer format only - OpenAI style)
authHeader := string(ctx.Request.Header.Peek("Authorization"))
if authHeader != "" {
// Only accept Bearer token format: "Bearer ..."
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
authHeaderValue := strings.TrimSpace(authHeader[7:]) // Remove "Bearer " prefix
if authHeaderValue != "" && !strings.HasPrefix(strings.ToLower(authHeaderValue), governance.VirtualKeyPrefix) {
apiKey = authHeaderValue
}
} else {
apiKey = authHeader
}
}
if apiKey == "" {
// Check x-api-key (Anthropic style) header if no valid Authorization header found
xAPIKey := string(ctx.Request.Header.Peek("x-api-key"))
if xAPIKey != "" && !strings.HasPrefix(strings.ToLower(xAPIKey), governance.VirtualKeyPrefix) {
apiKey = strings.TrimSpace(xAPIKey)
} else {
// Check x-goog-api-key (Google Gemini style) header if no valid Authorization header found
xGoogleAPIKey := string(ctx.Request.Header.Peek("x-goog-api-key"))
if xGoogleAPIKey != "" && !strings.HasPrefix(strings.ToLower(xGoogleAPIKey), governance.VirtualKeyPrefix) {
apiKey = strings.TrimSpace(xGoogleAPIKey)
}
}
}
// If we found an API key, create a Key object and store it in context
if apiKey != "" {
key := schemas.Key{
ID: "header-provided", // Identifier for header-provided keys
Value: *schemas.NewEnvVar(apiKey),
Models: schemas.WhiteList{"*"}, // Allow all models
Weight: 1.0, // Default weight
}
bifrostCtx.SetValue(schemas.BifrostContextKeyDirectKey, key)
}
}
return bifrostCtx, cancel
}
// BuildHTTPRequestFromFastHTTP creates an HTTPRequest from fasthttp context for streaming handlers.
// The returned request should be released with schemas.ReleaseHTTPRequest when done.
// Note: Body is not copied for streaming (body was already consumed for the request).
func BuildHTTPRequestFromFastHTTP(ctx *fasthttp.RequestCtx) *schemas.HTTPRequest {
req := schemas.AcquireHTTPRequest()
req.Method = string(ctx.Method())
req.Path = string(ctx.Path())
// Copy headers
for key, value := range ctx.Request.Header.All() {
req.Headers[string(key)] = string(value)
}
// Copy query params
for key, value := range ctx.Request.URI().QueryArgs().All() {
req.Query[string(key)] = string(value)
}
// Copy path parameters from user values
ctx.VisitUserValuesAll(func(key, value any) {
keyStr, keyIsString := key.(string)
valueStr, valueIsString := value.(string)
if !keyIsString || !valueIsString {
return
}
if strings.HasPrefix(keyStr, "bifrost-") ||
keyStr == "BifrostContextKeyRequestID" ||
keyStr == "trace_id" ||
keyStr == "span_id" {
return
}
req.PathParams[keyStr] = valueStr
})
// Note: Body not copied - for streaming, body was already consumed
return req
}
// BuildHTTPResponseFromFastHTTP creates an HTTPResponse snapshot from fasthttp context.
// Only captures status code and headers — body is skipped because for streaming
// responses it is an active io.Reader that cannot be materialized.
// The returned response should be released with schemas.ReleaseHTTPResponse when done.
func BuildHTTPResponseFromFastHTTP(ctx *fasthttp.RequestCtx) *schemas.HTTPResponse {
resp := schemas.AcquireHTTPResponse()
resp.StatusCode = ctx.Response.StatusCode()
for key, value := range ctx.Response.Header.All() {
resp.Headers[string(key)] = string(value)
}
return resp
}