diff --git a/lib/resty/openapi_validator/params.lua b/lib/resty/openapi_validator/params.lua index 6ca39dc..36d6634 100644 --- a/lib/resty/openapi_validator/params.lua +++ b/lib/resty/openapi_validator/params.lua @@ -453,7 +453,12 @@ function _M.validate(route, path_params, query_args, headers, skip) end if not skip.header and route.params.header then - validate_param_group(route.params.header, "header", headers) + -- Normalize header keys to lowercase for case-insensitive matching + local lower_headers = {} + for k, v in pairs(headers) do + lower_headers[str_lower(k)] = v + end + validate_param_group(route.params.header, "header", lower_headers) end if #errs > 0 then diff --git a/t/conformance/test_validate_header.lua b/t/conformance/test_validate_header.lua index 00596f5..bbb37d7 100644 --- a/t/conformance/test_validate_header.lua +++ b/t/conformance/test_validate_header.lua @@ -46,7 +46,33 @@ T.describe("header: missing required headers", function() T.like(err, "[Cc]ontent%-[Tt]ype", "error mentions Content-Type") end) --- TEST 3: skip header validation +-- TEST 3: case-insensitive header matching (spec has capitalized names, request uses exact match) +T.describe("header: case-insensitive match with exact case keys", function() + local ok, err = validator:validate_request({ + method = "GET", + path = "/validateHeaders", + headers = { + ["Authorization"] = "Bearer dGVzdA==.dGVzdA==.dGVzdA==", + ["Content-Type"] = "application/json", + }, + }) + T.ok(ok, "exact case headers pass: " .. tostring(err)) +end) + +-- TEST 4: case-insensitive header matching (spec has capitalized names, request all uppercase) +T.describe("header: case-insensitive match with uppercase keys", function() + local ok, err = validator:validate_request({ + method = "GET", + path = "/validateHeaders", + headers = { + ["AUTHORIZATION"] = "Bearer dGVzdA==.dGVzdA==.dGVzdA==", + ["CONTENT-TYPE"] = "application/json", + }, + }) + T.ok(ok, "uppercase headers pass: " .. tostring(err)) +end) + +-- TEST 5: skip header validation T.describe("header: skip header validation", function() local ok, err = validator:validate_request({ method = "GET", diff --git a/t/unit/test_params.lua b/t/unit/test_params.lua index 9d5aa59..35646f4 100644 --- a/t/unit/test_params.lua +++ b/t/unit/test_params.lua @@ -315,4 +315,42 @@ T.describe("params: deepObject anyOf composed (allOf) object branch", function() T.ok(not errs or #errs == 0, "no errors") end) +-- Header case-insensitive matching: spec has lowercase name, request has +-- canonical case key (e.g. HTTP/1.1 clients sending X-Client-Id). +T.describe("params: header case-insensitive match (canonical case)", function() + local route = make_route({ + { name = "x-client-id", ["in"] = "header", required = true, + schema = { type = "string" } }, + }, "header") + + local ok, errs = params_mod.validate(route, {}, {}, + { ["X-Client-Id"] = "test123" }) + T.ok(ok, "canonical case header found") + T.ok(not errs or #errs == 0, "no errors") +end) + +T.describe("params: header case-insensitive match (uppercase)", function() + local route = make_route({ + { name = "x-client-id", ["in"] = "header", required = true, + schema = { type = "string" } }, + }, "header") + + local ok, errs = params_mod.validate(route, {}, {}, + { ["X-CLIENT-ID"] = "test123" }) + T.ok(ok, "uppercase header found") + T.ok(not errs or #errs == 0, "no errors") +end) + +T.describe("params: header case-insensitive match (spec has mixed case)", function() + local route = make_route({ + { name = "Authorization", ["in"] = "header", required = true, + schema = { type = "string" } }, + }, "header") + + local ok, errs = params_mod.validate(route, {}, {}, + { ["authorization"] = "Bearer token" }) + T.ok(ok, "lowercase header matches mixed-case spec name") + T.ok(not errs or #errs == 0, "no errors") +end) + T.done()