Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- chore: added case-insensitive helper methods for header and query parameter lookups in HTTPRequest
39 changes: 37 additions & 2 deletions core/schemas/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package schemas

import (
"context"
"strings"
"sync"
)

Expand Down Expand Up @@ -30,11 +31,45 @@ type PluginStatus struct {
type HTTPRequest struct {
Method string `json:"method"`
Path string `json:"path"`
Headers map[string]string `json:"headers"` // keys are lowercase
Query map[string]string `json:"query"` // keys are lowercase
Headers map[string]string `json:"headers"`
Query map[string]string `json:"query"`
Body []byte `json:"body"`
}

// CaseInsensitiveHeaderLookup looks up a header key in a case-insensitive manner
func (req *HTTPRequest) CaseInsensitiveHeaderLookup(key string) string {
return caseInsensitiveLookup(req.Headers, key)
}

// CaseInsensitiveQueryLookup looks up a query key in a case-insensitive manner
func (req *HTTPRequest) CaseInsensitiveQueryLookup(key string) string {
return caseInsensitiveLookup(req.Query, key)
}

// caseInsensitiveLookup looks up a key in a case-insensitive manner for a map of strings
// Returns the value if found, otherwise an empty string
func caseInsensitiveLookup(data map[string]string, key string) string {
if data == nil || key == "" {
return ""
}
// exact match
if v, ok := data[key]; ok {
return v
}
// lower key checks
lowerKey := strings.ToLower(key)
if v, ok := data[lowerKey]; ok {
return v
}
// case-insensitive iteration
for k, v := range data {
if strings.EqualFold(k, key) {
return v
}
}
return ""
}

// HTTPResponse is a serializable representation of an HTTP response.
// Used for short-circuit responses in plugin HTTP transport interception.
type HTTPResponse struct {
Expand Down
30 changes: 20 additions & 10 deletions docs/plugins/writing-go-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,14 @@ func GetName() string {
// Only called when using HTTP transport (bifrost-http)
func HTTPTransportIntercept(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
fmt.Println("HTTPTransportIntercept called")
// Modify request in-place (headers, body, query params)

// Read headers using case-insensitive helper (recommended)
contentType := req.CaseInsensitiveHeaderLookup("Content-Type")
fmt.Printf("Content-Type: %s\n", contentType)

// Modify request in-place (use lowercase for direct map access)
req.Headers["x-custom-header"] = "custom-value"

// Return nil to continue, or return &schemas.HTTPResponse{} to short-circuit
return nil, nil
}
Expand Down Expand Up @@ -223,20 +229,24 @@ Key points:
- Return `(*HTTPResponse, nil)` to short-circuit with response
- Return `(nil, error)` to short-circuit with error

<Warning>
**Header and Query Parameter Lookups**: All header and query parameter keys in `req.Headers` and `req.Query` are **automatically normalized to lowercase** by Bifrost. When accessing or modifying these maps, always use lowercase keys:
<Note>
**Header and Query Parameter Lookups**: Use the case-insensitive helper methods for reading headers and query parameters:

```go
// ✅ Correct - use lowercase
value := req.Headers["content-type"]
req.Headers["x-custom-header"] = "value"
// ✅ Correct - use helper methods for case-insensitive lookup
contentType := req.CaseInsensitiveHeaderLookup("Content-Type")
apiKey := req.CaseInsensitiveQueryLookup("api_key")

// Also works with any casing
contentType := req.CaseInsensitiveHeaderLookup("content-type")
contentType := req.CaseInsensitiveHeaderLookup("CONTENT-TYPE")

// ❌ Wrong - uppercase won't match
value := req.Headers["Content-Type"] // Won't find the header
// For setting headers, use direct map access
req.Headers["X-Custom-Header"] = "value"
```

This ensures consistent, case-insensitive lookups regardless of how the client sent the headers.
</Warning>
The helper methods (`CaseInsensitiveHeaderLookup` and `CaseInsensitiveQueryLookup`) ensure your plugin works correctly regardless of how the client sends header/query parameter names.
</Note>

<Warning>
This function is **only called** when using `bifrost-http`. It's **not invoked** when using Bifrost as a Go SDK.
Expand Down
16 changes: 4 additions & 12 deletions docs/plugins/writing-wasm-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -751,19 +751,11 @@ Output: `build/plugin.wasm`

### http_intercept

<Warning>
**Header and Query Parameter Lookups**: All header and query parameter keys in the `request.headers` and `request.query` objects are **automatically normalized to lowercase** by Bifrost. When accessing or setting these fields, always use lowercase keys:

```json
// ✅ Correct - use lowercase
"headers": { "content-type": "application/json", "x-custom-header": "value" }
<Note>
**Header and Query Parameter Handling**: Headers and query parameters in `request.headers` and `request.query` preserve the original casing sent by the client. When looking up headers/query params, you should perform case-insensitive comparisons in your WASM plugin code to handle various casing (e.g., `Content-Type`, `content-type`, `CONTENT-TYPE`).

// ❌ Wrong - uppercase won't work consistently
"headers": { "Content-Type": "application/json" }
```

This ensures consistent, case-insensitive lookups regardless of how the client sent the headers.
</Warning>
For Go native plugins, use the built-in `CaseInsensitiveHeaderLookup()` and `CaseInsensitiveQueryLookup()` helper methods.
</Note>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**Input:**
```json
Expand Down
31 changes: 4 additions & 27 deletions plugins/governance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,36 +229,13 @@ func (p *GovernancePlugin) GetName() string {
return PluginName
}

// caseInsensitiveHeaderLookup looks up a header key in a case-insensitive manner
func caseInsensitiveLookup(data map[string]string, key string) string {
if data == nil || key == "" {
return ""
}
// exact match
if v, ok := data[key]; ok {
return v
}
// lower key checks
lowerKey := strings.ToLower(key)
if v, ok := data[lowerKey]; ok {
return v
}
// case-insensitive iteration
for k, v := range data {
if strings.EqualFold(k, key) {
return v
}
}
return ""
}

func parseVirtualKeyFromHTTPRequest(req *schemas.HTTPRequest) *string {
var virtualKeyValue string
vkHeader := caseInsensitiveLookup(req.Headers, "x-bf-vk")
vkHeader := req.CaseInsensitiveHeaderLookup("x-bf-vk")
if vkHeader != "" {
return bifrost.Ptr(vkHeader)
}
authHeader := caseInsensitiveLookup(req.Headers, "authorization")
authHeader := req.CaseInsensitiveHeaderLookup("authorization")
if authHeader != "" {
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
authHeaderValue := strings.TrimSpace(authHeader[7:]) // Remove "Bearer " prefix
Expand All @@ -270,12 +247,12 @@ func parseVirtualKeyFromHTTPRequest(req *schemas.HTTPRequest) *string {
if virtualKeyValue != "" {
return bifrost.Ptr(virtualKeyValue)
}
xAPIKey := caseInsensitiveLookup(req.Headers, "x-api-key")
xAPIKey := req.CaseInsensitiveHeaderLookup("x-api-key")
if xAPIKey != "" && strings.HasPrefix(strings.ToLower(xAPIKey), VirtualKeyPrefix) {
return bifrost.Ptr(xAPIKey)
}
// Checking x-goog-api-key header
xGoogleAPIKey := caseInsensitiveLookup(req.Headers, "x-goog-api-key")
xGoogleAPIKey := req.CaseInsensitiveHeaderLookup("x-goog-api-key")
if xGoogleAPIKey != "" && strings.HasPrefix(strings.ToLower(xGoogleAPIKey), VirtualKeyPrefix) {
return bifrost.Ptr(xGoogleAPIKey)
}
Expand Down