Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction {
response.write_status(status);
response.write_header("Content-Type", "application/json");
response.write_header("Access-Control-Allow-Origin", this->allowed_origin_);
// RFC 9110 §15.5.6: 405 responses MUST carry Allow listing supported
// methods. The MCP endpoint accepts POST and OPTIONS only.
// https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.6
if (std::string_view{status} ==
sourcemeta::one::STATUS_METHOD_NOT_ALLOWED) {
response.write_header("Allow", "POST, OPTIONS");
}
// Debuggability echo: not mandated by MCP, but lets clients confirm
// which protocol revision the server interpreted. For error paths
// where no negotiation succeeded, we echo the spec-default `2025-03-26`.
Expand Down
2 changes: 1 addition & 1 deletion src/actions/action_default_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class ActionDefault_v1 : public sourcemeta::one::RouterAction {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED,
"method-not-allowed", "This HTTP method is invalid for this URL",
this->error_schema_);
this->error_schema_, "GET, HEAD");
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/actions/action_health_check_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ActionHealthCheck_v1 : public sourcemeta::one::RouterAction {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED,
"method-not-allowed", "This HTTP method is invalid for this URL",
this->error_schema_);
this->error_schema_, "GET, HEAD");
return;
}

Expand Down
10 changes: 7 additions & 3 deletions src/actions/action_jsonschema_evaluate_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class ActionJSONSchemaEvaluate_v1 : public sourcemeta::one::RouterAction {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED,
"method-not-allowed", "This HTTP method is invalid for this URL",
error_schema);
error_schema, "POST, OPTIONS");
return;
}

Expand All @@ -170,11 +170,15 @@ class ActionJSONSchemaEvaluate_v1 : public sourcemeta::one::RouterAction {
}

if (!evaluation_enabled.has_value()) {
// RFC 9110 §15.5.6: Allow lists the methods this specific target
// resource currently supports. POST hits this very branch (returns
// 405) when the schema was not precompiled, so only OPTIONS is
// actually supported on this URL.
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED,
"no-schema-template",
"This schema was not precompiled for schema evaluation",
error_schema);
"This schema was not precompiled for schema evaluation", error_schema,
"OPTIONS");
return;
}

Expand Down
7 changes: 7 additions & 0 deletions src/actions/action_mcp_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction {
response.write_status(status);
response.write_header("Content-Type", "application/json");
response.write_header("Access-Control-Allow-Origin", this->allowed_origin_);
// RFC 9110 §15.5.6: 405 responses MUST carry Allow listing supported
// methods. The MCP endpoint accepts POST and OPTIONS only.
// https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.6
if (std::string_view{status} ==
sourcemeta::one::STATUS_METHOD_NOT_ALLOWED) {
response.write_header("Allow", "POST, OPTIONS");
}
// Debuggability echo: not mandated by MCP, but lets clients confirm
// which protocol revision the server interpreted. For error paths
// where no negotiation succeeded, we echo the spec-default `2025-03-26`.
Expand Down
2 changes: 1 addition & 1 deletion src/actions/action_schema_search_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::RouterAction {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED,
"method-not-allowed", "This HTTP method is invalid for this URL",
this->error_schema_);
this->error_schema_, "GET");
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/actions/action_serve_static_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class ActionServeStatic_v1 : public sourcemeta::one::RouterAction {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED,
"method-not-allowed", "This HTTP method is invalid for this URL",
this->error_schema_);
this->error_schema_, "GET, HEAD");
return;
}

Expand Down
9 changes: 7 additions & 2 deletions src/http/include/sourcemeta/one/http_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ inline auto send_response(const char *const code, const HTTPRequest &request,
// See https://www.rfc-editor.org/rfc/rfc7807
inline auto json_error(const HTTPRequest &request, HTTPResponse &response,
const char *const code, std::string &&identifier,
std::string &&message, const std::string_view schema)
-> void {
std::string &&message, const std::string_view schema,
const std::string_view allow = {}) -> void {
auto object{sourcemeta::core::JSON::make_object()};
object.assign("title", sourcemeta::core::JSON{"sourcemeta:one/" +
std::move(identifier)});
Expand All @@ -64,6 +64,11 @@ inline auto json_error(const HTTPRequest &request, HTTPResponse &response,
response.write_status(code);
response.write_header("Content-Type", "application/problem+json");
response.write_header("Access-Control-Allow-Origin", "*");
// RFC 9110 §15.5.6: 405 responses MUST carry Allow listing supported methods.
// https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.6
if (!allow.empty() && std::string_view{code} == STATUS_METHOD_NOT_ALLOWED) {
response.write_header("Allow", allow);
}
if (!schema.empty()) {
write_link_header(response, schema);
}
Expand Down
2 changes: 1 addition & 1 deletion src/router/artifact.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ auto RouterAction::artifact_serve(
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED,
"method-not-allowed", "This HTTP method is invalid for this URL",
error_schema);
error_schema, "GET, HEAD");
return;
}

Expand Down
3 changes: 3 additions & 0 deletions test/e2e/empty/hurl/mcp-2025-11-25.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ GET {{base}}/self/v1/mcp
HTTP 405
Content-Type: application/json
Access-Control-Allow-Origin: http://localhost:8000
Allow: POST, OPTIONS
Link: </self/v1/schemas/mcp/response>; rel="describedby"
MCP-Protocol-Version: 2025-03-26
[Captures]
Expand Down Expand Up @@ -33,6 +34,7 @@ DELETE {{base}}/self/v1/mcp
HTTP 405
Content-Type: application/json
Access-Control-Allow-Origin: http://localhost:8000
Allow: POST, OPTIONS
Link: </self/v1/schemas/mcp/response>; rel="describedby"
MCP-Protocol-Version: 2025-03-26
[Captures]
Expand Down Expand Up @@ -64,6 +66,7 @@ PUT {{base}}/self/v1/mcp
HTTP 405
Content-Type: application/json
Access-Control-Allow-Origin: http://localhost:8000
Allow: POST, OPTIONS
Link: </self/v1/schemas/mcp/response>; rel="describedby"
MCP-Protocol-Version: 2025-03-26
[Captures]
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/headless/hurl/fetch.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ POST {{base}}/test/schemas/string.json
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -228,6 +229,7 @@ DELETE {{base}}/test/schemas/string.json
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/headless/hurl/health.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ POST {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -35,6 +36,7 @@ PUT {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -47,6 +49,7 @@ DELETE {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -59,6 +62,7 @@ PATCH {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -71,6 +75,7 @@ OPTIONS {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/headless/hurl/no-static.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ POST {{base}}/self/v1/static/style.min.css
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -37,6 +38,7 @@ PUT {{base}}/self/v1/static/style.min.css
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -49,6 +51,7 @@ DELETE {{base}}/self/v1/static/style.min.css
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-dependencies.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ POST {{base}}/self/v1/api/schemas/dependencies/test/schemas/no-id
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-dependents.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ POST {{base}}/self/v1/api/schemas/dependents/test/schemas/bundling-double
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-evaluate.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Content-Type: application/problem+json
"Hello World"
HTTP 405
Access-Control-Allow-Origin: *
Allow: OPTIONS
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-locations.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ POST {{base}}/self/v1/api/schemas/locations/test/schemas/no-id
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-metadata.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ POST {{base}}/self/v1/api/schemas/metadata/test/schemas/no-id
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-positions.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ POST {{base}}/self/v1/api/schemas/positions/test/schemas/no-id
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-search.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ POST {{base}}/self/v1/api/schemas/search?q=foo
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-stats.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ POST {{base}}/self/v1/api/schemas/stats/test/schemas/no-id
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/headless/hurl/schemas-trace.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Content-Type: application/problem+json
"Hello World"
HTTP 405
Access-Control-Allow-Origin: *
Allow: OPTIONS
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/html/hurl/fetch.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ POST {{base}}/test/schemas/string.json
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -390,6 +391,7 @@ DELETE {{base}}/test/schemas/string.json
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/html/hurl/health.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ POST {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -35,6 +36,7 @@ PUT {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -47,6 +49,7 @@ DELETE {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -59,6 +62,7 @@ PATCH {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand All @@ -71,6 +75,7 @@ OPTIONS {{base}}/self/v1/health
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/html/hurl/html.all.hurl
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
OPTIONS {{base}}
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
[Asserts]
header "Referrer-Policy" not exists
header "Content-Security-Policy" not exists
Expand Down
1 change: 1 addition & 0 deletions test/e2e/html/hurl/schemas-dependencies.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ POST {{base}}/self/v1/api/schemas/dependencies/test/v2.0/schema
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/html/hurl/schemas-dependents.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ POST {{base}}/self/v1/api/schemas/dependents/test/bundling/double
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/html/hurl/schemas-evaluate.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Content-Type: application/problem+json
"Hello World"
HTTP 405
Access-Control-Allow-Origin: *
Allow: OPTIONS
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
1 change: 1 addition & 0 deletions test/e2e/html/hurl/schemas-locations.all.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ POST {{base}}/self/v1/api/schemas/locations/test/v2.0/schema
HTTP 405
Content-Type: application/problem+json
Access-Control-Allow-Origin: *
Allow: GET, HEAD
Link: </self/v1/schemas/api/error>; rel="describedby"
[Captures]
last_response: body
Expand Down
Loading
Loading