diff --git a/mote/middleware.lua b/mote/middleware.lua index 9aabe23..f19e568 100644 --- a/mote/middleware.lua +++ b/mote/middleware.lua @@ -18,11 +18,12 @@ local cors_base = { ["Access-Control-Allow-Headers"] = "Content-Type, Authorization", } -function middleware.cors_headers() +function middleware.cors_headers(methods) local headers = {} for k, v in pairs(cors_base) do headers[k] = v end + if methods then headers["Access-Control-Allow-Methods"] = methods end return headers end diff --git a/mote/router.lua b/mote/router.lua index 19faeeb..dd0e503 100644 --- a/mote/router.lua +++ b/mote/router.lua @@ -94,6 +94,23 @@ function router.match(method, path) return nil end +function router.methods_for_path(path) + local methods = {} + local found = false + for _, route in ipairs(routes) do + local captures = route.pattern:match(path) + if captures then + found = true + if route.method == "*" then + return nil -- wildcard matches all methods, no restriction + end + methods[route.method] = true + end + end + if not found then return nil end + return methods +end + function router.clear() routes = {} middleware_stack = {} diff --git a/mote/server.lua b/mote/server.lua index d7fbd31..fe28e8e 100644 --- a/mote/server.lua +++ b/mote/server.lua @@ -13,6 +13,7 @@ local url_mod = require("mote.url") local concat = table.concat local insert = table.insert +local sort = table.sort local server = {} @@ -444,6 +445,17 @@ local function should_keep_alive(http_version, headers, wrapper, config) return tonumber(http_version) >= 1.1 end +local function build_allow_header(path_methods) + if not path_methods then return nil end + local list = {} + for m in pairs(path_methods) do + list[#list + 1] = m + end + list[#list + 1] = "OPTIONS" + sort(list) + return concat(list, ", ") +end + local function handle_request(wrapper, config) wrapper.request_count = wrapper.request_count + 1 local start_time = socket.gettime() @@ -476,7 +488,8 @@ local function handle_request(wrapper, config) local query = req.location.query if middleware.is_preflight(req.method) then - local cors = middleware.cors_headers() + local allow = build_allow_header(router.methods_for_path(path)) + local cors = middleware.cors_headers(allow) cors["Content-Length"] = "0" send_response(wrapper, 204, cors, nil, keep_alive) log.info("http", req.method .. " " .. path .. " 204") @@ -519,10 +532,19 @@ local function handle_request(wrapper, config) handler, params = router.match("GET", path) end if not handler then - local cors = middleware.cors_headers() - cors["Content-Type"] = "application/json" - send_response(wrapper, 404, cors, middleware.encode_json({ error = "not found" }), keep_alive) - log.info("http", req.method .. " " .. path .. " 404") + local allow = build_allow_header(router.methods_for_path(path)) + if allow then + local cors = middleware.cors_headers(allow) + cors["Content-Type"] = "application/json" + cors["Allow"] = allow + send_response(wrapper, 405, cors, middleware.encode_json({ error = "method not allowed" }), keep_alive) + log.info("http", req.method .. " " .. path .. " 405") + else + local cors = middleware.cors_headers() + cors["Content-Type"] = "application/json" + send_response(wrapper, 404, cors, middleware.encode_json({ error = "not found" }), keep_alive) + log.info("http", req.method .. " " .. path .. " 404") + end return true, keep_alive, nil end diff --git a/spec/router_spec.lua b/spec/router_spec.lua index 2a21bfc..9e2169a 100644 --- a/spec/router_spec.lua +++ b/spec/router_spec.lua @@ -49,6 +49,29 @@ describe("router", function() assert.is_nil(router.match("GET", "/not-exists")) end) + describe("methods_for_path", function() + it("returns methods registered for a path", function() + router.get("/resource", function() end) + router.post("/resource", function() end) + + local methods = router.methods_for_path("/resource") + assert.is_truthy(methods) + assert.is_true(methods["GET"]) + assert.is_true(methods["POST"]) + assert.is_nil(methods["DELETE"]) + end) + + it("returns nil for unknown paths", function() + router.get("/exists", function() end) + assert.is_nil(router.methods_for_path("/not-exists")) + end) + + it("returns nil for wildcard routes", function() + router.all("/anything", function() end) + assert.is_nil(router.methods_for_path("/anything")) + end) + end) + describe("middleware", function() it("runs middleware before handler", function() local order = {}