From ac637ca4d272d7152d711d7d5ced963751f4e723 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 8 May 2026 17:57:23 +0800 Subject: [PATCH 1/2] fix: header parameter lookup should be case-insensitive Normalize header keys to lowercase before validation so that spec-defined lowercase names (e.g. x-client-id) match request headers in canonical case (e.g. X-Client-Id) from HTTP/1.1 clients. Previously the fallback str_lower(name) only lowercased the spec name, which is a no-op when the spec name is already lowercase. --- lib/resty/openapi_validator/params.lua | 7 ++++- t/conformance/test_validate_header.lua | 28 ++++++++++++++++++- t/unit/test_params.lua | 38 ++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) 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..36d5e86 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 lowercase, request canonical case) +T.describe("header: case-insensitive match with canonical 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, "canonical case headers pass: " .. tostring(err)) +end) + +-- TEST 4: case-insensitive header matching (spec lowercase, request mixed case) +T.describe("header: case-insensitive match with mixed 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, "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() From 41a7c3ac25a49c89fa194520046e78c49cfbd100 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 8 May 2026 18:03:13 +0800 Subject: [PATCH 2/2] fix: correct test descriptions per review feedback - Spec has capitalized names (Authorization, Content-Type), not lowercase - Rename 'mixed case' test to 'uppercase' to match actual header keys used --- t/conformance/test_validate_header.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/t/conformance/test_validate_header.lua b/t/conformance/test_validate_header.lua index 36d5e86..bbb37d7 100644 --- a/t/conformance/test_validate_header.lua +++ b/t/conformance/test_validate_header.lua @@ -46,8 +46,8 @@ T.describe("header: missing required headers", function() T.like(err, "[Cc]ontent%-[Tt]ype", "error mentions Content-Type") end) --- TEST 3: case-insensitive header matching (spec lowercase, request canonical case) -T.describe("header: case-insensitive match with canonical case keys", function() +-- 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", @@ -56,11 +56,11 @@ T.describe("header: case-insensitive match with canonical case keys", function() ["Content-Type"] = "application/json", }, }) - T.ok(ok, "canonical case headers pass: " .. tostring(err)) + T.ok(ok, "exact case headers pass: " .. tostring(err)) end) --- TEST 4: case-insensitive header matching (spec lowercase, request mixed case) -T.describe("header: case-insensitive match with mixed case keys", function() +-- 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",