mr-668: align GET /graphs 405 body code with HTTP status

The single-mode `GET /graphs` handler returned an `ApiError` built
via struct literal with `status: METHOD_NOT_ALLOWED, code: BadRequest`.
The body code disagreed with the HTTP status — clients deserializing
on `code` saw `bad_request`, clients deserializing on `status` saw
405. Same bug class as the earlier 503+Conflict mismatch on the
removed YAML drift path.

Close the class for this one remaining instance:

* Add `ErrorCode::MethodNotAllowed` to the API enum.
* Add `ApiError::method_not_allowed(msg)` — pairs the 405 status
  with the matching code.
* Replace the struct literal in `server_graphs_list` with the
  constructor.
* Regenerate `openapi.json` (adds `method_not_allowed` to the
  ErrorCode schema enum).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ragnor Comerford 2026-05-27 12:04:51 +02:00
parent dedb633c62
commit 5b19a03003
No known key found for this signature in database
3 changed files with 23 additions and 7 deletions

View file

@ -344,6 +344,11 @@ pub enum ErrorCode {
Forbidden,
BadRequest,
NotFound,
/// 405 Method Not Allowed — the route exists but the active server
/// mode doesn't serve this method (e.g. `GET /graphs` in single-graph
/// mode). Distinct from 404 so clients can tell "wrong context" from
/// "no such resource."
MethodNotAllowed,
Conflict,
/// 429 Too Many Requests — per-actor admission cap exceeded.
/// Clients should respect the `Retry-After` header.

View file

@ -567,6 +567,20 @@ impl ApiError {
}
}
/// HTTP 405 Method Not Allowed. Used when the route is mounted but
/// the active server mode doesn't serve it (`GET /graphs` in
/// single-graph mode returns this instead of 404 so clients can
/// distinguish "wrong context" from "no such resource").
pub fn method_not_allowed(message: impl Into<String>) -> Self {
Self {
status: StatusCode::METHOD_NOT_ALLOWED,
code: ErrorCode::MethodNotAllowed,
message: message.into(),
merge_conflicts: Vec::new(),
manifest_conflict: None,
}
}
pub fn conflict(message: impl Into<String>) -> Self {
Self {
status: StatusCode::CONFLICT,
@ -1155,13 +1169,9 @@ async fn server_graphs_list(
// 405 in single mode — there's no registry to enumerate, and the
// legacy URL surface didn't expose this endpoint.
if matches!(state.mode(), ServerMode::Single { .. }) {
return Err(ApiError {
status: StatusCode::METHOD_NOT_ALLOWED,
code: ErrorCode::BadRequest,
message: "GET /graphs is only available in multi-graph mode".to_string(),
merge_conflicts: Vec::new(),
manifest_conflict: None,
});
return Err(ApiError::method_not_allowed(
"GET /graphs is only available in multi-graph mode",
));
}
// Server-level Cedar gate. `state.server_policy` is loaded from

View file

@ -1256,6 +1256,7 @@
"forbidden",
"bad_request",
"not_found",
"method_not_allowed",
"conflict",
"too_many_requests",
"internal"