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) }