diff --git a/enterprise/e2e/path/hurl/mcp-2025-11-25-lifecycle.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-11-25-lifecycle.all.hurl index aa23d38ed..a68375dc6 100644 --- a/enterprise/e2e/path/hurl/mcp-2025-11-25-lifecycle.all.hurl +++ b/enterprise/e2e/path/hurl/mcp-2025-11-25-lifecycle.all.hurl @@ -244,8 +244,14 @@ header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ OPTIONS {{base}}/self/v1/mcp -HTTP 404 +HTTP 204 Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists diff --git a/src/actions/action_default_v1.h b/src/actions/action_default_v1.h index cb5fd4314..a8f8b4d73 100644 --- a/src/actions/action_default_v1.h +++ b/src/actions/action_default_v1.h @@ -45,6 +45,12 @@ class ActionDefault_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept, Accept-Encoding, If-None-Match, " + "If-Modified-Since"); + return; + } // Browser-targeted security headers we apply to every HTML response: // // - Referrer-Policy (W3C Referrer Policy): @@ -91,7 +97,7 @@ class ActionDefault_v1 : public sourcemeta::one::RouterAction { request, response, sourcemeta::core::HTTP_STATUS_METHOD_NOT_ALLOWED, "sourcemeta:one/method-not-allowed", "This HTTP method is invalid for this URL", this->error_schema_, - "*", "GET, HEAD"); + "*", "GET, HEAD, OPTIONS"); return; } if (sourcemeta::core::http_match_accept( @@ -180,7 +186,7 @@ class ActionDefault_v1 : public sourcemeta::one::RouterAction { request, response, sourcemeta::core::HTTP_STATUS_METHOD_NOT_ALLOWED, "sourcemeta:one/method-not-allowed", "This HTTP method is invalid for this URL", this->error_schema_, - "*", "GET, HEAD"); + "*", "GET, HEAD, OPTIONS"); } else { sourcemeta::one::json_error( request, response, sourcemeta::core::HTTP_STATUS_NOT_FOUND, diff --git a/src/actions/action_dependency_tree_v1.h b/src/actions/action_dependency_tree_v1.h index c86a872dc..c892eae2d 100644 --- a/src/actions/action_dependency_tree_v1.h +++ b/src/actions/action_dependency_tree_v1.h @@ -55,6 +55,12 @@ class ActionDependencyTree_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span matches, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept-Encoding, If-None-Match, " + "If-Modified-Since"); + return; + } if (matches.empty()) { sourcemeta::one::json_error( request, response, diff --git a/src/actions/action_health_check_v1.h b/src/actions/action_health_check_v1.h index 13166762c..9cb092a99 100644 --- a/src/actions/action_health_check_v1.h +++ b/src/actions/action_health_check_v1.h @@ -40,12 +40,17 @@ class ActionHealthCheck_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept, Accept-Encoding"); + return; + } if (request.method() != "get" && request.method() != "head") { sourcemeta::one::json_error( request, response, sourcemeta::core::HTTP_STATUS_METHOD_NOT_ALLOWED, "sourcemeta:one/method-not-allowed", "This HTTP method is invalid for this URL", this->error_schema_, "*", - "GET, HEAD"); + "GET, HEAD, OPTIONS"); return; } diff --git a/src/actions/action_jsonschema_serve_v1.h b/src/actions/action_jsonschema_serve_v1.h index 5ec735c67..3488c8b5a 100644 --- a/src/actions/action_jsonschema_serve_v1.h +++ b/src/actions/action_jsonschema_serve_v1.h @@ -85,6 +85,12 @@ class ActionJSONSchemaServe_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span matches, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept, Accept-Encoding, If-None-Match, " + "If-Modified-Since"); + return; + } serve(*this, matches.front(), request, response, this->error_schema_); } diff --git a/src/actions/action_list_directory_v1.h b/src/actions/action_list_directory_v1.h index 368ef54f7..cd01b25a4 100644 --- a/src/actions/action_list_directory_v1.h +++ b/src/actions/action_list_directory_v1.h @@ -51,6 +51,12 @@ class ActionListDirectory_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span matches, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept-Encoding, If-None-Match, " + "If-Modified-Since"); + return; + } const std::string_view path_match{matches.empty() ? std::string_view{} : matches.front()}; const auto path{ diff --git a/src/actions/action_schema_search_v1.h b/src/actions/action_schema_search_v1.h index 4eff05501..5633aadbc 100644 --- a/src/actions/action_schema_search_v1.h +++ b/src/actions/action_schema_search_v1.h @@ -56,6 +56,11 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept, Accept-Encoding"); + return; + } // RFC 9110 §9.3.2: "The HEAD method is identical to GET except that the // server MUST NOT send content in the response. [...] The server SHOULD @@ -67,7 +72,7 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::RouterAction { request, response, sourcemeta::core::HTTP_STATUS_METHOD_NOT_ALLOWED, "sourcemeta:one/method-not-allowed", "This HTTP method is invalid for this URL", this->error_schema_, "*", - "GET, HEAD"); + "GET, HEAD, OPTIONS"); return; } diff --git a/src/actions/action_serve_explorer_artifact_v1.h b/src/actions/action_serve_explorer_artifact_v1.h index 2ffc08bce..77a8e64b9 100644 --- a/src/actions/action_serve_explorer_artifact_v1.h +++ b/src/actions/action_serve_explorer_artifact_v1.h @@ -50,6 +50,12 @@ class ActionServeExplorerArtifact_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span matches, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept-Encoding, If-None-Match, " + "If-Modified-Since"); + return; + } if (!matches.empty() && (matches.front().find('#') != std::string_view::npos || matches.front().find("%23") != std::string_view::npos)) { diff --git a/src/actions/action_serve_schema_artifact_v1.h b/src/actions/action_serve_schema_artifact_v1.h index 963f26b73..fc1a87b90 100644 --- a/src/actions/action_serve_schema_artifact_v1.h +++ b/src/actions/action_serve_schema_artifact_v1.h @@ -51,6 +51,12 @@ class ActionServeSchemaArtifact_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span matches, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept-Encoding, If-None-Match, " + "If-Modified-Since"); + return; + } if (matches.empty()) { sourcemeta::one::json_error( request, response, diff --git a/src/actions/action_serve_static_v1.h b/src/actions/action_serve_static_v1.h index 0d1f85731..f27aebda2 100644 --- a/src/actions/action_serve_static_v1.h +++ b/src/actions/action_serve_static_v1.h @@ -42,13 +42,19 @@ class ActionServeStatic_v1 : public sourcemeta::one::RouterAction { auto rest(const std::span matches, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void override { + if (request.method() == "options") { + sourcemeta::one::cors_preflight(request, response, "GET, HEAD, OPTIONS", + "Accept-Encoding, If-None-Match, " + "If-Modified-Since"); + return; + } if (this->file_root_.empty()) { if (request.method() != "get" && request.method() != "head") { sourcemeta::one::json_error( request, response, sourcemeta::core::HTTP_STATUS_METHOD_NOT_ALLOWED, "sourcemeta:one/method-not-allowed", "This HTTP method is invalid for this URL", this->error_schema_, - "*", "GET, HEAD"); + "*", "GET, HEAD, OPTIONS"); return; } diff --git a/src/http/include/sourcemeta/one/http_helpers.h b/src/http/include/sourcemeta/one/http_helpers.h index 24170894b..78fb875d1 100644 --- a/src/http/include/sourcemeta/one/http_helpers.h +++ b/src/http/include/sourcemeta/one/http_helpers.h @@ -52,6 +52,37 @@ inline auto send_response(const sourcemeta::core::HTTPStatus &status, std::format("{} {} {}", status.wire, request.method(), request.path())); } +// RFC 9110 §9.3.7: OPTIONS responses describe communication options +// for the target resource. Fetch §3.2.2 (CORS preflight): non-simple +// cross-origin requests issue an OPTIONS preflight whose ACK shape +// (status 204 + Access-Control-Allow-*) governs whether the actual +// request fires. The per-surface `allow_methods` and `allow_headers` +// are required so each action declares its own contract explicitly, +// matching the K and L disciplines. +// https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.7 +// https://fetch.spec.whatwg.org/#cors-preflight-fetch +inline auto cors_preflight(const HTTPRequest &request, HTTPResponse &response, + const std::string_view allow_methods, + const std::string_view allow_headers) -> void { + assert(!allow_methods.empty()); + assert(!allow_headers.empty()); + assert(allow_methods.find_first_of("\r\n") == std::string_view::npos); + assert(allow_headers.find_first_of("\r\n") == std::string_view::npos); + response.write_status(sourcemeta::core::HTTP_STATUS_NO_CONTENT); + response.write_header("Access-Control-Allow-Origin", "*"); + response.write_header("Access-Control-Expose-Headers", "Link, ETag"); + response.write_header("Access-Control-Allow-Methods", allow_methods); + response.write_header("Access-Control-Allow-Headers", allow_headers); + response.write_header("Access-Control-Max-Age", "3600"); + // Browser preflight cache is governed by `Access-Control-Max-Age`; + // `no-store` keeps shared HTTP caches from storing this response. + response.write_header("Cache-Control", "no-store"); + // RFC 9110 §9.3.7: OPTIONS responses SHOULD include Allow. Different + // audience than Access-Control-Allow-Methods (HTTP vs CORS preflight). + response.write_header("Allow", allow_methods); + send_response(sourcemeta::core::HTTP_STATUS_NO_CONTENT, request, response); +} + // CORS scope is required at every error site. No default for `origin` so a // caller cannot silently widen a restricted-origin handler to wildcard. An // empty origin means the route is CORS-disabled and no Allow-Origin or diff --git a/src/router/artifact.cc b/src/router/artifact.cc index a95a98f86..e0e8cdf40 100644 --- a/src/router/artifact.cc +++ b/src/router/artifact.cc @@ -128,12 +128,19 @@ auto RouterAction::artifact_serve( // the default action is Accept-branched, others just gzip), so // the caller has to pick the right stack. assert(!vary.empty()); + // OPTIONS preflight is a per-surface concern (the Allow-Headers axis + // varies by action), so each caller short-circuits it before reaching + // here via `cors_preflight`. If this assertion fires, a caller is + // missing that early branch and the 405 emitted below would advertise + // `Allow: GET, HEAD, OPTIONS` while simultaneously refusing OPTIONS, + // which is internally inconsistent. + assert(request.method() != "options"); if (request.method() != "get" && request.method() != "head") { sourcemeta::one::json_error( request, response, sourcemeta::core::HTTP_STATUS_METHOD_NOT_ALLOWED, "sourcemeta:one/method-not-allowed", "This HTTP method is invalid for this URL", error_schema, - enable_cors ? "*" : "", "GET, HEAD"); + enable_cors ? "*" : "", "GET, HEAD, OPTIONS"); return; } diff --git a/test/e2e/headless/hurl/explorer.all.hurl b/test/e2e/headless/hurl/explorer.all.hurl index 7f0ef0d9d..ec97049eb 100644 --- a/test/e2e/headless/hurl/explorer.all.hurl +++ b/test/e2e/headless/hurl/explorer.all.hurl @@ -24,7 +24,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/headless/hurl/fetch.all.hurl b/test/e2e/headless/hurl/fetch.all.hurl index 8754159c1..dd4dbb0e5 100644 --- a/test/e2e/headless/hurl/fetch.all.hurl +++ b/test/e2e/headless/hurl/fetch.all.hurl @@ -244,7 +244,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -262,7 +262,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -302,7 +302,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response_405: body diff --git a/test/e2e/headless/hurl/health.all.hurl b/test/e2e/headless/hurl/health.all.hurl index 8a5e40b33..b9715f4bc 100644 --- a/test/e2e/headless/hurl/health.all.hurl +++ b/test/e2e/headless/hurl/health.all.hurl @@ -31,7 +31,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -48,7 +48,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -65,7 +65,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -82,7 +82,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -94,18 +94,17 @@ jsonpath "$.type" == "sourcemeta:one/method-not-allowed" jsonpath "$.title" == "Method Not Allowed" OPTIONS {{base}}/self/v1/health -HTTP 405 +HTTP 204 Cache-Control: no-store -Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists header "Content-Security-Policy" not exists header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ -jsonpath "$.status" == 405 -jsonpath "$.type" == "sourcemeta:one/method-not-allowed" -jsonpath "$.title" == "Method Not Allowed" diff --git a/test/e2e/headless/hurl/no-static.all.hurl b/test/e2e/headless/hurl/no-static.all.hurl index 19166b55c..09c33ec00 100644 --- a/test/e2e/headless/hurl/no-static.all.hurl +++ b/test/e2e/headless/hurl/no-static.all.hurl @@ -34,7 +34,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -51,7 +51,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -68,7 +68,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists diff --git a/test/e2e/headless/hurl/not-found.all.hurl b/test/e2e/headless/hurl/not-found.all.hurl index c7b9470c7..4f91970d5 100644 --- a/test/e2e/headless/hurl/not-found.all.hurl +++ b/test/e2e/headless/hurl/not-found.all.hurl @@ -69,4 +69,3 @@ header "Content-Security-Policy" not exists header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ jsonpath "$.valid" == true - diff --git a/test/e2e/headless/hurl/preflight.all.hurl b/test/e2e/headless/hurl/preflight.all.hurl new file mode 100644 index 000000000..41f82064c --- /dev/null +++ b/test/e2e/headless/hurl/preflight.all.hurl @@ -0,0 +1,116 @@ +# RFC 9110 §9.3.7 + Fetch §3.2.2: each read-only surface advertises its +# own CORS preflight contract. Cover at least one representative endpoint +# per action surface so the response shape is locked in independently of +# the per-feature test files. + +OPTIONS {{base}}/self/v1/health +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/search +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/list/test/doc +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/metadata/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/dependencies/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/health/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ diff --git a/test/e2e/headless/hurl/schemas-dependencies.all.hurl b/test/e2e/headless/hurl/schemas-dependencies.all.hurl index 23fc7e7a8..b00e646f7 100644 --- a/test/e2e/headless/hurl/schemas-dependencies.all.hurl +++ b/test/e2e/headless/hurl/schemas-dependencies.all.hurl @@ -164,7 +164,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/headless/hurl/schemas-dependents.all.hurl b/test/e2e/headless/hurl/schemas-dependents.all.hurl index 0b34e8319..39aa752a7 100644 --- a/test/e2e/headless/hurl/schemas-dependents.all.hurl +++ b/test/e2e/headless/hurl/schemas-dependents.all.hurl @@ -182,7 +182,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/headless/hurl/schemas-locations.all.hurl b/test/e2e/headless/hurl/schemas-locations.all.hurl index 3b2b152d4..9ad19ac0d 100644 --- a/test/e2e/headless/hurl/schemas-locations.all.hurl +++ b/test/e2e/headless/hurl/schemas-locations.all.hurl @@ -109,7 +109,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/headless/hurl/schemas-metadata.all.hurl b/test/e2e/headless/hurl/schemas-metadata.all.hurl index 04ffdf65f..52066fef9 100644 --- a/test/e2e/headless/hurl/schemas-metadata.all.hurl +++ b/test/e2e/headless/hurl/schemas-metadata.all.hurl @@ -136,7 +136,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/headless/hurl/schemas-positions.all.hurl b/test/e2e/headless/hurl/schemas-positions.all.hurl index caf99f020..b29763cbd 100644 --- a/test/e2e/headless/hurl/schemas-positions.all.hurl +++ b/test/e2e/headless/hurl/schemas-positions.all.hurl @@ -77,7 +77,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/headless/hurl/schemas-search.all.hurl b/test/e2e/headless/hurl/schemas-search.all.hurl index 98eb26c8a..b26ce5252 100644 --- a/test/e2e/headless/hurl/schemas-search.all.hurl +++ b/test/e2e/headless/hurl/schemas-search.all.hurl @@ -148,7 +148,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/headless/hurl/schemas-stats.all.hurl b/test/e2e/headless/hurl/schemas-stats.all.hurl index 584280aa6..87016c19a 100644 --- a/test/e2e/headless/hurl/schemas-stats.all.hurl +++ b/test/e2e/headless/hurl/schemas-stats.all.hurl @@ -60,7 +60,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/fetch.all.hurl b/test/e2e/html/hurl/fetch.all.hurl index 9389e7959..3ac5536dc 100644 --- a/test/e2e/html/hurl/fetch.all.hurl +++ b/test/e2e/html/hurl/fetch.all.hurl @@ -420,7 +420,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -455,7 +455,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists diff --git a/test/e2e/html/hurl/health.all.hurl b/test/e2e/html/hurl/health.all.hurl index 8a5e40b33..b9715f4bc 100644 --- a/test/e2e/html/hurl/health.all.hurl +++ b/test/e2e/html/hurl/health.all.hurl @@ -31,7 +31,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -48,7 +48,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -65,7 +65,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -82,7 +82,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -94,18 +94,17 @@ jsonpath "$.type" == "sourcemeta:one/method-not-allowed" jsonpath "$.title" == "Method Not Allowed" OPTIONS {{base}}/self/v1/health -HTTP 405 +HTTP 204 Cache-Control: no-store -Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists header "Content-Security-Policy" not exists header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ -jsonpath "$.status" == 405 -jsonpath "$.type" == "sourcemeta:one/method-not-allowed" -jsonpath "$.title" == "Method Not Allowed" diff --git a/test/e2e/html/hurl/html.all.hurl b/test/e2e/html/hurl/html.all.hurl index f67a11774..858f34a55 100644 --- a/test/e2e/html/hurl/html.all.hurl +++ b/test/e2e/html/hurl/html.all.hurl @@ -1,34 +1,18 @@ OPTIONS {{base}} -HTTP 405 +HTTP 204 Cache-Control: no-store -Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD -Link: ; rel="describedby" -[Captures] -last_response: body -schema_path: header "Link" regex "<([^>]+)>" +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists header "Content-Security-Policy" not exists header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ -jsonpath "$.type" == "sourcemeta:one/method-not-allowed" -jsonpath "$.title" == "Method Not Allowed" -jsonpath "$.status" == 405 -jsonpath "$.detail" == "This HTTP method is invalid for this URL" - -POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} -``` -{{last_response}} -``` -HTTP 200 -Cache-Control: no-store -[Asserts] -header "Vary" not exists -jsonpath "$.valid" == true # Regression: an HTML-preferring client doing a write method on the root URL # must see 405 (Allow lists the supported methods), not 404. Content @@ -40,7 +24,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body @@ -73,7 +57,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body @@ -106,7 +90,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body @@ -463,7 +447,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body @@ -498,7 +482,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body @@ -531,7 +515,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/not-found.all.hurl b/test/e2e/html/hurl/not-found.all.hurl index c7b9470c7..4f91970d5 100644 --- a/test/e2e/html/hurl/not-found.all.hurl +++ b/test/e2e/html/hurl/not-found.all.hurl @@ -69,4 +69,3 @@ header "Content-Security-Policy" not exists header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ jsonpath "$.valid" == true - diff --git a/test/e2e/html/hurl/preflight.all.hurl b/test/e2e/html/hurl/preflight.all.hurl new file mode 100644 index 000000000..e892c5660 --- /dev/null +++ b/test/e2e/html/hurl/preflight.all.hurl @@ -0,0 +1,132 @@ +# RFC 9110 §9.3.7 + Fetch §3.2.2: each read-only surface advertises its +# own CORS preflight contract. Cover at least one representative endpoint +# per action surface so the response shape is locked in independently of +# the per-feature test files. + +OPTIONS {{base}}/self/v1/health +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/search +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/list/test/doc +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/metadata/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/dependencies/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/api/schemas/health/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/self/v1/static/style.min.css +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ diff --git a/test/e2e/html/hurl/schemas-dependencies.all.hurl b/test/e2e/html/hurl/schemas-dependencies.all.hurl index 000e94134..cf8fbe41e 100644 --- a/test/e2e/html/hurl/schemas-dependencies.all.hurl +++ b/test/e2e/html/hurl/schemas-dependencies.all.hurl @@ -276,7 +276,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/schemas-dependents.all.hurl b/test/e2e/html/hurl/schemas-dependents.all.hurl index bbedb65ac..ab44e0261 100644 --- a/test/e2e/html/hurl/schemas-dependents.all.hurl +++ b/test/e2e/html/hurl/schemas-dependents.all.hurl @@ -294,7 +294,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/schemas-locations.all.hurl b/test/e2e/html/hurl/schemas-locations.all.hurl index 908e39d8d..960bf3cd4 100644 --- a/test/e2e/html/hurl/schemas-locations.all.hurl +++ b/test/e2e/html/hurl/schemas-locations.all.hurl @@ -109,7 +109,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/schemas-metadata.all.hurl b/test/e2e/html/hurl/schemas-metadata.all.hurl index d0817c705..53a9cf64c 100644 --- a/test/e2e/html/hurl/schemas-metadata.all.hurl +++ b/test/e2e/html/hurl/schemas-metadata.all.hurl @@ -138,7 +138,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/schemas-positions.all.hurl b/test/e2e/html/hurl/schemas-positions.all.hurl index 54ef4d797..6f30cbaa7 100644 --- a/test/e2e/html/hurl/schemas-positions.all.hurl +++ b/test/e2e/html/hurl/schemas-positions.all.hurl @@ -77,7 +77,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/schemas-search.all.hurl b/test/e2e/html/hurl/schemas-search.all.hurl index f39991282..18bb3a579 100644 --- a/test/e2e/html/hurl/schemas-search.all.hurl +++ b/test/e2e/html/hurl/schemas-search.all.hurl @@ -148,7 +148,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/html/hurl/schemas-stats.all.hurl b/test/e2e/html/hurl/schemas-stats.all.hurl index 2e177b89c..601e19c52 100644 --- a/test/e2e/html/hurl/schemas-stats.all.hurl +++ b/test/e2e/html/hurl/schemas-stats.all.hurl @@ -60,7 +60,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS Link: ; rel="describedby" [Captures] last_response: body diff --git a/test/e2e/no-api/hurl/fetch.all.hurl b/test/e2e/no-api/hurl/fetch.all.hurl index bf540b54b..795256615 100644 --- a/test/e2e/no-api/hurl/fetch.all.hurl +++ b/test/e2e/no-api/hurl/fetch.all.hurl @@ -86,7 +86,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -108,7 +108,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists diff --git a/test/e2e/path/hurl/fetch-draft3.all.hurl b/test/e2e/path/hurl/fetch-draft3.all.hurl index 8905033e4..630dfc18d 100644 --- a/test/e2e/path/hurl/fetch-draft3.all.hurl +++ b/test/e2e/path/hurl/fetch-draft3.all.hurl @@ -157,7 +157,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists diff --git a/test/e2e/path/hurl/fetch.all.hurl b/test/e2e/path/hurl/fetch.all.hurl index b43833498..8eef74b46 100644 --- a/test/e2e/path/hurl/fetch.all.hurl +++ b/test/e2e/path/hurl/fetch.all.hurl @@ -347,7 +347,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists diff --git a/test/e2e/path/hurl/health.all.hurl b/test/e2e/path/hurl/health.all.hurl index 2d34e01f0..f129492ad 100644 --- a/test/e2e/path/hurl/health.all.hurl +++ b/test/e2e/path/hurl/health.all.hurl @@ -31,7 +31,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -48,7 +48,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -65,7 +65,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -82,7 +82,7 @@ Cache-Control: no-store Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists @@ -94,18 +94,17 @@ jsonpath "$.type" == "sourcemeta:one/method-not-allowed" jsonpath "$.title" == "Method Not Allowed" OPTIONS {{base}}/v1/catalog/self/v1/health -HTTP 405 +HTTP 204 Cache-Control: no-store -Content-Type: application/problem+json Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Link, ETag -Allow: GET, HEAD +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists header "Content-Security-Policy" not exists header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ -jsonpath "$.status" == 405 -jsonpath "$.type" == "sourcemeta:one/method-not-allowed" -jsonpath "$.title" == "Method Not Allowed" diff --git a/test/e2e/path/hurl/mcp-2025-11-25.all.hurl b/test/e2e/path/hurl/mcp-2025-11-25.all.hurl index 75723e147..f27a1750b 100644 --- a/test/e2e/path/hurl/mcp-2025-11-25.all.hurl +++ b/test/e2e/path/hurl/mcp-2025-11-25.all.hurl @@ -162,8 +162,14 @@ header "X-Frame-Options" not exists header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ OPTIONS {{base}}/self/v1/mcp -HTTP 404 +HTTP 204 Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS [Asserts] header "Vary" not exists header "Referrer-Policy" not exists diff --git a/test/e2e/path/hurl/preflight.all.hurl b/test/e2e/path/hurl/preflight.all.hurl new file mode 100644 index 000000000..e153b04b3 --- /dev/null +++ b/test/e2e/path/hurl/preflight.all.hurl @@ -0,0 +1,116 @@ +# RFC 9110 §9.3.7 + Fetch §3.2.2: each read-only surface advertises its +# own CORS preflight contract. Cover at least one representative endpoint +# per action surface so the response shape is locked in independently of +# the per-feature test files. + +OPTIONS {{base}}/v1/catalog/self/v1/health +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/v1/catalog/self/v1/api/schemas/search +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/v1/catalog/self/v1/api/list/test/doc +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/v1/catalog/self/v1/api/schemas/metadata/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/v1/catalog/self/v1/api/schemas/dependencies/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/v1/catalog/self/v1/api/schemas/health/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/ + +OPTIONS {{base}}/v1/catalog/test/schemas/string +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding, If-None-Match, If-Modified-Since +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS +[Asserts] +header "Vary" not exists +header "Referrer-Policy" not exists +header "Content-Security-Policy" not exists +header "X-Frame-Options" not exists +header "Date" matches /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (0[1-9]|[12][0-9]|3[01]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} ([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9] GMT$/