diff --git a/core/changelog.md b/core/changelog.md
index e69de29bb2..e9993725b0 100644
--- a/core/changelog.md
+++ b/core/changelog.md
@@ -0,0 +1 @@
+- chore: added case-insensitive helper methods for header and query parameter lookups in HTTPRequest
\ No newline at end of file
diff --git a/core/schemas/plugin.go b/core/schemas/plugin.go
index 968be20cec..1bd794b870 100644
--- a/core/schemas/plugin.go
+++ b/core/schemas/plugin.go
@@ -3,6 +3,7 @@ package schemas
import (
"context"
+ "strings"
"sync"
)
@@ -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 {
diff --git a/docs/plugins/writing-go-plugin.mdx b/docs/plugins/writing-go-plugin.mdx
index 5516b242dc..a5e13a9e7f 100644
--- a/docs/plugins/writing-go-plugin.mdx
+++ b/docs/plugins/writing-go-plugin.mdx
@@ -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
}
@@ -223,20 +229,24 @@ Key points:
- Return `(*HTTPResponse, nil)` to short-circuit with response
- Return `(nil, error)` to short-circuit with error
-
-**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:
+
+**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.
-
+The helper methods (`CaseInsensitiveHeaderLookup` and `CaseInsensitiveQueryLookup`) ensure your plugin works correctly regardless of how the client sends header/query parameter names.
+
This function is **only called** when using `bifrost-http`. It's **not invoked** when using Bifrost as a Go SDK.
diff --git a/docs/plugins/writing-wasm-plugin.mdx b/docs/plugins/writing-wasm-plugin.mdx
index 3e2a88d814..885bc11c3c 100644
--- a/docs/plugins/writing-wasm-plugin.mdx
+++ b/docs/plugins/writing-wasm-plugin.mdx
@@ -751,19 +751,11 @@ Output: `build/plugin.wasm`
### http_intercept
-
-**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" }
+
+**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.
-
+For Go native plugins, use the built-in `CaseInsensitiveHeaderLookup()` and `CaseInsensitiveQueryLookup()` helper methods.
+
**Input:**
```json
diff --git a/plugins/governance/main.go b/plugins/governance/main.go
index 5da7e348cf..c2588d8ee8 100644
--- a/plugins/governance/main.go
+++ b/plugins/governance/main.go
@@ -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
@@ -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)
}