mirror of
https://github.com/versity/versitygw.git
synced 2026-09-24 08:54:47 +00:00
rdma: serialize RC route errors at the route boundary
The RC control routes returned fiber.Error values for 400, 404, 409, 429, 502, and 503 outcomes, but the production S3 error handler converts ordinary fiber errors into a generic 500 response, so clients observed InternalError for every protocol outcome. The routes now send the final status and S3-style XML body themselves through a shared terminal serializer. S3-aware errors from authentication, authorization, and the object backend keep their status and code. Session-server failures map to protocol error codes: InvalidRdmaRequest, NoSuchRdmaSession, RdmaSessionConflict, RdmaResourceLimit, RdmaTransferFailed, and RdmaServiceUnavailable. Owner mismatch answers the same 404 as an unknown session so a session id is never disclosed across principals. The platform stub keeps its 501 answer and uses the same response shape.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an "AS
|
||||
// IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
// express or implied. See the License for the specific language
|
||||
// governing permissions and limitations under the License.
|
||||
|
||||
package rcroutes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
// writeRouteError renders err as the terminal S3-style XML response
|
||||
// of an RC control route. The production S3 error handler converts
|
||||
// ordinary Fiber errors into a generic 500 response, so the route
|
||||
// must send its final status and body itself. Errors that already
|
||||
// carry S3 semantics (authentication, authorization, object
|
||||
// backend) keep their status and code; anything else is mapped to
|
||||
// a protocol error without exposing internal detail.
|
||||
func writeRouteError(ctx fiber.Ctx, err error) error {
|
||||
requestID, hostID := utils.EnsureRequestIDs(ctx)
|
||||
|
||||
var apiErr s3err.APIError
|
||||
switch e := classifyRouteError(err).(type) {
|
||||
case s3err.S3Error:
|
||||
apiErr = e.BaseError()
|
||||
case s3err.APIError:
|
||||
apiErr = e
|
||||
}
|
||||
if isRouteNotImplemented(err) {
|
||||
apiErr = s3err.APIError{
|
||||
Code: "NotImplemented",
|
||||
Description: "RDMA is not supported on this platform",
|
||||
HTTPStatusCode: fiber.StatusNotImplemented,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML)
|
||||
return ctx.Status(apiErr.HTTPStatusCode).
|
||||
Send(apiErr.XMLBody(requestID, hostID))
|
||||
}
|
||||
|
||||
// classifyRouteError resolves err to a value implementing
|
||||
// s3err.S3Error. S3-aware errors pass through unchanged; RC
|
||||
// transport errors are mapped to the closest protocol error.
|
||||
func classifyRouteError(err error) s3err.S3Error {
|
||||
var s3Err s3err.S3Error
|
||||
if errors.As(err, &s3Err) {
|
||||
return s3Err
|
||||
}
|
||||
|
||||
code, status, description := routeErrorDetails(err)
|
||||
return s3err.APIError{
|
||||
Code: code,
|
||||
Description: description,
|
||||
HTTPStatusCode: status,
|
||||
}
|
||||
}
|
||||
|
||||
// routeErrorDetails maps a non-S3 route error to its protocol
|
||||
// error code, HTTP status, and description.
|
||||
func routeErrorDetails(err error) (code string, status int, description string) {
|
||||
switch {
|
||||
case isRouteBadRequest(err):
|
||||
return "InvalidRdmaRequest", fiber.StatusBadRequest,
|
||||
"Invalid RDMA control request"
|
||||
case isRouteNotFound(err):
|
||||
return "NoSuchRdmaSession", fiber.StatusNotFound,
|
||||
"No such RDMA session"
|
||||
case isRouteConflict(err):
|
||||
return "RdmaSessionConflict", fiber.StatusConflict,
|
||||
"RDMA session state conflict"
|
||||
case isRouteLimit(err):
|
||||
return "RdmaResourceLimit", fiber.StatusTooManyRequests,
|
||||
"RDMA resource limit reached"
|
||||
case isRouteBadGateway(err):
|
||||
return "RdmaTransferFailed", fiber.StatusBadGateway,
|
||||
"RDMA transfer failed"
|
||||
case isRouteUnavailable(err):
|
||||
return "RdmaServiceUnavailable", fiber.StatusServiceUnavailable,
|
||||
"RDMA service is shutting down"
|
||||
default:
|
||||
return s3err.GetAPIError(s3err.ErrInternalError).Code,
|
||||
fiber.StatusInternalServerError, "Internal Error"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an "AS
|
||||
// IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
// express or implied. See the License for the specific language
|
||||
// governing permissions and limitations under the License.
|
||||
|
||||
//go:build linux && amd64 && cgo
|
||||
|
||||
package rcroutes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/versity/versitygw/rdma/rcserver"
|
||||
)
|
||||
|
||||
// This file keeps the Linux-only error classifiers next to the
|
||||
// shared terminal serializer in errors.go. The marker types below
|
||||
// carry only the intended protocol class; the HTTP status and XML
|
||||
// body are decided in one place.
|
||||
|
||||
// errRouteBadRequest marks a malformed control request (invalid
|
||||
// header value, bad argument).
|
||||
type errRouteBadRequest struct{}
|
||||
|
||||
func (errRouteBadRequest) Error() string { return "invalid RDMA control request" }
|
||||
|
||||
// errRouteUnavailable marks admission refusal during shutdown.
|
||||
type errRouteUnavailable struct{}
|
||||
|
||||
func (errRouteUnavailable) Error() string {
|
||||
return "RDMA service is shutting down"
|
||||
}
|
||||
|
||||
// isRouteBadRequest reports whether err is a malformed-request
|
||||
// class error from the Linux route handlers.
|
||||
func isRouteBadRequest(err error) bool {
|
||||
var bad errRouteBadRequest
|
||||
return errors.As(err, &bad)
|
||||
}
|
||||
|
||||
// isRouteNotFound reports whether err identifies an unknown or
|
||||
// cross-principal session; both map to the same response so the
|
||||
// session id is not disclosed across principals.
|
||||
func isRouteNotFound(err error) bool {
|
||||
return errors.Is(err, rcserver.ErrNoSession)
|
||||
}
|
||||
|
||||
// isRouteConflict reports whether err is a stale, duplicate, or
|
||||
// wrong-state session error.
|
||||
func isRouteConflict(err error) bool {
|
||||
return errors.Is(err, rcserver.ErrStale) ||
|
||||
errors.Is(err, rcserver.ErrState) ||
|
||||
errors.Is(err, rcserver.ErrDouble)
|
||||
}
|
||||
|
||||
// isRouteLimit reports whether err was caused by a configured
|
||||
// resource limit.
|
||||
func isRouteLimit(err error) bool {
|
||||
return errors.Is(err, rcserver.ErrLimit)
|
||||
}
|
||||
|
||||
// isRouteBadGateway reports whether the RC wire transfer failed.
|
||||
func isRouteBadGateway(err error) bool {
|
||||
return errors.Is(err, rcserver.ErrWire)
|
||||
}
|
||||
|
||||
// isRouteUnavailable reports whether the RC service refused
|
||||
// admission or is shutting down.
|
||||
func isRouteUnavailable(err error) bool {
|
||||
var unavail errRouteUnavailable
|
||||
return errors.As(err, &unavail)
|
||||
}
|
||||
|
||||
// isRouteNotImplemented reports whether the platform stub answered
|
||||
// the request; always false on Linux builds.
|
||||
func isRouteNotImplemented(err error) bool { return false }
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an "AS
|
||||
// IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
// express or implied. See the License for the specific language
|
||||
// governing permissions and limitations under the License.
|
||||
|
||||
//go:build !(linux && amd64 && cgo)
|
||||
|
||||
package rcroutes
|
||||
|
||||
import "errors"
|
||||
|
||||
// errRouteNotImplemented marks the platform-stub answer of the
|
||||
// control routes.
|
||||
type errRouteNotImplemented struct{}
|
||||
|
||||
func (errRouteNotImplemented) Error() string {
|
||||
return "RDMA not supported on this platform"
|
||||
}
|
||||
|
||||
// isRouteNotImplemented reports whether the platform stub answered
|
||||
// the request.
|
||||
func isRouteNotImplemented(err error) bool {
|
||||
var ni errRouteNotImplemented
|
||||
return errors.As(err, &ni)
|
||||
}
|
||||
|
||||
// isRouteBadRequest reports whether err is a malformed-request
|
||||
// class error. The stub never classifies request errors because it
|
||||
// answers before validation.
|
||||
func isRouteBadRequest(err error) bool { return false }
|
||||
|
||||
// isRouteNotFound reports whether err identifies an unknown
|
||||
// session. Never true on the stub.
|
||||
func isRouteNotFound(err error) bool { return false }
|
||||
|
||||
// isRouteConflict reports whether err is a session-state conflict.
|
||||
// Never true on the stub.
|
||||
func isRouteConflict(err error) bool { return false }
|
||||
|
||||
// isRouteLimit reports whether err was caused by a resource
|
||||
// limit. Never true on the stub.
|
||||
func isRouteLimit(err error) bool { return false }
|
||||
|
||||
// isRouteBadGateway reports whether the RC wire transfer failed.
|
||||
// Never true on the stub.
|
||||
func isRouteBadGateway(err error) bool { return false }
|
||||
|
||||
// isRouteUnavailable reports whether the RC service refused
|
||||
// admission. Never true on the stub.
|
||||
func isRouteUnavailable(err error) bool { return false }
|
||||
@@ -97,18 +97,27 @@ func principalID(acct auth.Account) rcserver.PrincipalID {
|
||||
}
|
||||
|
||||
func errNotAdmitted() error {
|
||||
return fiber.NewError(fiber.StatusServiceUnavailable,
|
||||
"RDMA service is shutting down")
|
||||
return errRouteUnavailable{}
|
||||
}
|
||||
|
||||
func invalidHeader(name, value string) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
fmt.Sprintf("invalid %s header: %q", name, value))
|
||||
return fmt.Errorf("invalid %s header: %q: %w",
|
||||
name, value, errRouteBadRequest{})
|
||||
}
|
||||
|
||||
// Prepare handles PREPARE: authorize the object access, create the
|
||||
// session, and (GET) stage the object into the session buffer.
|
||||
func (h *Handler) Prepare(ctx fiber.Ctx) error {
|
||||
// Serialize any error at the route boundary: the
|
||||
// production S3 error handler turns ordinary Fiber
|
||||
// errors into a generic 500 response.
|
||||
if err := h.prepareCore(ctx); err != nil {
|
||||
return writeRouteError(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) prepareCore(ctx fiber.Ctx) error {
|
||||
if !h.svc.TryEnter() {
|
||||
return errNotAdmitted()
|
||||
}
|
||||
@@ -256,6 +265,16 @@ func (h *Handler) stageGet(ctx fiber.Ctx, sessionID, bucket, key string,
|
||||
// the session's target, then run the transfer. PUT picks up the
|
||||
// received data and hands it to the object backend.
|
||||
func (h *Handler) Ready(ctx fiber.Ctx) error {
|
||||
// Serialize any error at the route boundary: the
|
||||
// production S3 error handler turns ordinary Fiber
|
||||
// errors into a generic 500 response.
|
||||
if err := h.readyCore(ctx); err != nil {
|
||||
return writeRouteError(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) readyCore(ctx fiber.Ctx) error {
|
||||
if !h.svc.TryEnter() {
|
||||
return errNotAdmitted()
|
||||
}
|
||||
@@ -295,7 +314,7 @@ func (h *Handler) Ready(ctx fiber.Ctx) error {
|
||||
if err != nil {
|
||||
// Owner mismatch and unknown session both answer 404 so
|
||||
// the session id is not disclosed cross-principal.
|
||||
return fiber.NewError(fiber.StatusNotFound, "no such RDMA session")
|
||||
return mapRcError(err)
|
||||
}
|
||||
|
||||
// Re-run authorization for the session's stored target and
|
||||
@@ -303,7 +322,7 @@ func (h *Handler) Ready(ctx fiber.Ctx) error {
|
||||
// request.
|
||||
bucket, key, ok := splitTarget(info.Target)
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "session target")
|
||||
return errors.New("invalid session target")
|
||||
}
|
||||
if err := h.authorize(ctx, acct, isRoot, bucket, key, info.Op == 1); err != nil {
|
||||
// Permission revoked mid-session: cancel the session.
|
||||
@@ -335,7 +354,7 @@ func (h *Handler) Ready(ctx fiber.Ctx) error {
|
||||
// rolled the claim back (state Prepared, no completion ref),
|
||||
// so answer 409 without any finalizer.
|
||||
if resp.Outcome == rcserver.ReadyBusy {
|
||||
return fiber.NewError(fiber.StatusConflict, "peer busy")
|
||||
return fmt.Errorf("peer busy: %w", rcserver.ErrDouble)
|
||||
}
|
||||
|
||||
// The claim succeeded: from here until the response commits,
|
||||
@@ -439,6 +458,16 @@ func (h *Handler) commitPut(ctx fiber.Ctx, sessionID, bucket, key string,
|
||||
|
||||
// Cancel handles CANCEL: authenticated owner tears the session down.
|
||||
func (h *Handler) Cancel(ctx fiber.Ctx) error {
|
||||
// Serialize any error at the route boundary: the
|
||||
// production S3 error handler turns ordinary Fiber
|
||||
// errors into a generic 500 response.
|
||||
if err := h.cancelCore(ctx); err != nil {
|
||||
return writeRouteError(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) cancelCore(ctx fiber.Ctx) error {
|
||||
if !h.svc.TryEnter() {
|
||||
return errNotAdmitted()
|
||||
}
|
||||
@@ -583,30 +612,15 @@ func formatHex(v uint64) string {
|
||||
}
|
||||
|
||||
// mapRcError translates ABI statuses into HTTP-shaped failures.
|
||||
// mapRcError wraps a session-server error so the shared terminal
|
||||
// serializer in errors.go can classify it. Owner mismatch answers
|
||||
// the same 404 as an unknown session so a session id is never
|
||||
// disclosed across principals.
|
||||
func mapRcError(err error) error {
|
||||
var status int
|
||||
var msg string
|
||||
switch {
|
||||
case errors.Is(err, rcserver.ErrNoSession):
|
||||
status, msg = fiber.StatusNotFound, "no such RDMA session"
|
||||
case errors.Is(err, rcserver.ErrStale):
|
||||
status, msg = fiber.StatusConflict, "stale RDMA session handle"
|
||||
case errors.Is(err, rcserver.ErrSession):
|
||||
status, msg = fiber.StatusForbidden, "RDMA session owner mismatch"
|
||||
case errors.Is(err, rcserver.ErrState), errors.Is(err, rcserver.ErrDouble):
|
||||
status, msg = fiber.StatusConflict, "wrong RDMA session state"
|
||||
case errors.Is(err, rcserver.ErrLimit):
|
||||
status, msg = fiber.StatusTooManyRequests, "RDMA resource limit"
|
||||
case errors.Is(err, rcserver.ErrWire):
|
||||
status, msg = fiber.StatusBadGateway, "RDMA transfer failed"
|
||||
case errors.Is(err, rcserver.ErrShort):
|
||||
status, msg = fiber.StatusBadRequest, "short RDMA transfer"
|
||||
case errors.Is(err, rcserver.ErrTrunc):
|
||||
status, msg = fiber.StatusBadRequest, "RDMA value too long"
|
||||
case errors.Is(err, rcserver.ErrArg):
|
||||
status, msg = fiber.StatusBadRequest, "invalid RDMA argument"
|
||||
default:
|
||||
status, msg = fiber.StatusInternalServerError, "RDMA internal error"
|
||||
return fmt.Errorf("session owner mismatch: %w",
|
||||
rcserver.ErrNoSession)
|
||||
}
|
||||
return fiber.NewError(status, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -34,16 +34,17 @@ func New(svc any, be backend.Backend, iam auth.IAMService,
|
||||
return &Handler{}
|
||||
}
|
||||
|
||||
func notImplemented() error {
|
||||
return fiber.NewError(fiber.StatusNotImplemented,
|
||||
"RDMA not supported on this platform")
|
||||
// notImplemented answers 501 through the shared terminal error
|
||||
// serializer so the response shape matches the Linux routes.
|
||||
func notImplemented(ctx fiber.Ctx) error {
|
||||
return writeRouteError(ctx, errRouteNotImplemented{})
|
||||
}
|
||||
|
||||
// Prepare is a stub handler that answers 501 Not Implemented.
|
||||
func (h *Handler) Prepare(ctx fiber.Ctx) error { return notImplemented() }
|
||||
func (h *Handler) Prepare(ctx fiber.Ctx) error { return notImplemented(ctx) }
|
||||
|
||||
// Ready is a stub handler that answers 501 Not Implemented.
|
||||
func (h *Handler) Ready(ctx fiber.Ctx) error { return notImplemented() }
|
||||
func (h *Handler) Ready(ctx fiber.Ctx) error { return notImplemented(ctx) }
|
||||
|
||||
// Cancel is a stub handler that answers 501 Not Implemented.
|
||||
func (h *Handler) Cancel(ctx fiber.Ctx) error { return notImplemented() }
|
||||
func (h *Handler) Cancel(ctx fiber.Ctx) error { return notImplemented(ctx) }
|
||||
|
||||
Reference in New Issue
Block a user