Skip to content

Commit

Permalink
Automatically return gRPC errors for framework-level errors (#4813)
Browse files Browse the repository at this point in the history
  • Loading branch information
jumaffre authored Jan 10, 2023
1 parent 68a4b38 commit 5e0d4f3
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 107 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- `ccf::RpcContext::set_response()` has been renamed to `ccf::RpcContext::set_response_json()` (#4813).

## [4.0.0-dev3]

[4.0.0-dev3]: https://github.com/microsoft/CCF/releases/tag/ccf-4.0.0-dev3
Expand Down
41 changes: 9 additions & 32 deletions include/ccf/rpc_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,44 +149,21 @@ namespace ccf
set_response_trailer(kv.first, kv.second);
}

/// Construct OData-formatted response to capture multiple error details
virtual void set_response_json(
nlohmann::json& body, http_status status) = 0;

/// Construct error response, formatted according to the request content
/// type (either JSON OData-formatted or gRPC error)
virtual void set_error(
http_status status,
const std::string& code,
std::string&& msg,
std::vector<ccf::ODataErrorDetails>& details)
{
nlohmann::json body =
ccf::ODataErrorResponse{ccf::ODataError{code, std::move(msg), details}};
set_response(body, status);
}
const std::vector<ccf::ODataErrorDetails>& details = {}) = 0;

/// Construct OData-formatted error response.
virtual void set_error(
http_status status, const std::string& code, std::string&& msg)
{
set_error(ccf::ErrorDetails{status, code, std::move(msg)});
}
/// Construct error response, formatted according to the request content
/// type (either JSON OData-formatted or gRPC error)
virtual void set_error(ccf::ErrorDetails&& error) = 0;

/// Construct OData-formatted error response.
virtual void set_error(ccf::ErrorDetails&& error)
{
nlohmann::json body = ccf::ODataErrorResponse{
ccf::ODataError{std::move(error.code), std::move(error.msg)}};
set_response(body, error.status);
}

virtual void set_response(nlohmann::json& body, http_status status)
{
// Set error_handler to replace, to avoid throwing if the error message
// contains non-UTF8 characters. Other args are default values
const auto s =
body.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
set_response_status(status);
set_response_body(std::vector<uint8_t>(s.begin(), s.end()));
set_response_header(
http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
}
///@}

/// \defgroup Framework metadata
Expand Down
6 changes: 3 additions & 3 deletions samples/apps/logging/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ namespace loggingapp
},
}};

ctx.rpc_ctx->set_response(response, HTTP_STATUS_BAD_REQUEST);
ctx.rpc_ctx->set_response_json(response, HTTP_STATUS_BAD_REQUEST);
return;
}

Expand All @@ -524,7 +524,7 @@ namespace loggingapp
if (record.has_value())
{
nlohmann::json response = LoggingGet::Out{record.value()};
ctx.rpc_ctx->set_response(response, HTTP_STATUS_OK);
ctx.rpc_ctx->set_response_json(response, HTTP_STATUS_OK);
return;
}

Expand All @@ -537,7 +537,7 @@ namespace loggingapp
},
}};

ctx.rpc_ctx->set_response(response, HTTP_STATUS_BAD_REQUEST);
ctx.rpc_ctx->set_response_json(response, HTTP_STATUS_BAD_REQUEST);
};

make_read_only_endpoint(
Expand Down
114 changes: 114 additions & 0 deletions src/endpoints/grpc/grpc_status.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/http_consts.h"
#include "ccf/http_header_map.h"

#include <string>

#define GRPC_STATUS_MAP(XX) \
XX(0, OK, "Ok") \
XX(1, CANCELLED, "Cancelled") \
XX(2, UNKNOWN, "Unknown") \
XX(3, INVALID_ARGUMENT, "Invalid Argument") \
XX(4, DEADLINE_EXCEEDED, "Deadline Exceeded") \
XX(5, NOT_FOUND, "Not Found") \
XX(6, ALREADY_EXISTS, "Already Exists") \
XX(7, PERMISSION_DENIED, "Permission Denied") \
XX(8, RESOURCE_EXHAUSTED, "Resource Exhausted") \
XX(9, FAILED_PRECONDITION, "Failed Precondition") \
XX(10, ABORTED, "Aborted") \
XX(11, OUT_OF_RANGE, "Out Of Range") \
XX(12, UNIMPLEMENTED, "Unimplemented") \
XX(13, INTERNAL, "Internal") \
XX(14, UNAVAILABLE, "Unavailable") \
XX(15, DATA_LOSS, "Data Loss") \
XX(16, UNAUTHENTICATED, "Unauthenticated")

enum grpc_status
{
#define XX(num, name, string) GRPC_STATUS_##name = num,
GRPC_STATUS_MAP(XX)
#undef XX
};

static inline const char* grpc_status_str(enum grpc_status s)
{
switch (s)
{
#define XX(num, name, string) \
case GRPC_STATUS_##name: \
return string;
GRPC_STATUS_MAP(XX)
#undef XX
default:
return "<unknown>";
}
}

// CCF is primarily an HTTP framework. However, gRPC clients should be returned
// an appropriate gRPC status code when an error is returned at the framework
// level (e.g. authentication error) so we use the following function to
// automatically convert HTTP errors to gRPC statuses.
// Inspired by
// https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
static grpc_status http_status_to_grpc(enum http_status s)
{
// Note: GRPC_STATUS_CANCELLED, GRPC_STATUS_ABORTED and GRPC_STATUS_DATA_LOSS
// are currently never returned
switch (s)
{
case HTTP_STATUS_UNAUTHORIZED: // 401
return GRPC_STATUS_UNAUTHENTICATED;
case HTTP_STATUS_FORBIDDEN: // 404
return GRPC_STATUS_PERMISSION_DENIED;
case HTTP_STATUS_NOT_FOUND: // 404
return GRPC_STATUS_NOT_FOUND;
case HTTP_STATUS_CONFLICT: // 409
return GRPC_STATUS_ALREADY_EXISTS;
case HTTP_STATUS_PRECONDITION_FAILED: // 412
return GRPC_STATUS_FAILED_PRECONDITION;
case HTTP_STATUS_RANGE_NOT_SATISFIABLE: // 416
return GRPC_STATUS_OUT_OF_RANGE;
case HTTP_STATUS_TOO_MANY_REQUESTS: // 429
return GRPC_STATUS_RESOURCE_EXHAUSTED;
case HTTP_STATUS_NOT_IMPLEMENTED: // 501
return GRPC_STATUS_UNIMPLEMENTED;
case HTTP_STATUS_SERVICE_UNAVAILABLE: // 503
return GRPC_STATUS_UNAVAILABLE;
case HTTP_STATUS_GATEWAY_TIMEOUT: // 504
return GRPC_STATUS_DEADLINE_EXCEEDED;
default:
{
// For non-specific codes, we approximate to the closest generic code
if (s >= 200 && s < 300) // 2xx
return GRPC_STATUS_OK;
else if (s >= 400 && s < 500) // 4xx
return GRPC_STATUS_INVALID_ARGUMENT;
else if (s >= 500) // 5xx
return GRPC_STATUS_INTERNAL;
else
return GRPC_STATUS_UNKNOWN;
}
}
}

namespace ccf::grpc
{
static const http::HeaderMap default_response_headers = {
{http::headers::CONTENT_TYPE, http::headervalues::contenttype::GRPC}};

static constexpr auto TRAILER_STATUS = "grpc-status";
static constexpr auto TRAILER_MESSAGE = "grpc-message";

static http::HeaderKeyValue make_status_trailer(int32_t code)
{
return {TRAILER_STATUS, std::to_string(code)};
}

static http::HeaderKeyValue make_message_trailer(const std::string& msg)
{
return {TRAILER_MESSAGE, msg};
}
}
49 changes: 4 additions & 45 deletions src/endpoints/grpc/status.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,20 @@
// Licensed under the Apache 2.0 License.
#pragma once

#include "grpc_status.h"
#include "status.pb.h"

#include <optional>
#include <string>

// Mapping to HTTP errors as per
// https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
#define GRPC_STATUS_MAP(XX) \
XX(0, OK, "Ok", 200) \
XX(1, CANCELLED, "Cancelled", 499) \
XX(2, UNKNOWN, "Unknown", 500) \
XX(3, INVALID_ARGUMENT, "Invalid Argument", 400) \
XX(4, DEADLINE_EXCEEDED, "Deadline Exceeded", 504) \
XX(5, NOT_FOUND, "Not Found", 404) \
XX(6, ALREADY_EXISTS, "Already Exists", 409) \
XX(7, PERMISSION_DENIED, "Permission Denied", 403) \
XX(8, RESOURCE_EXHAUSTED, "Resource Exhausted", 429) \
XX(9, FAILED_PRECONDITION, "Failed Precondition", 400) \
XX(10, ABORTED, "Aborted", 409) \
XX(11, OUT_OF_RANGE, "Out Of Range", 400) \
XX(12, UNIMPLEMENTED, "Unimplemented", 501) \
XX(13, INTERNAL, "Internal", 500) \
XX(14, UNAVAILABLE, "Unavailable", 503) \
XX(15, DATA_LOSS, "Data Loss", 500) \
XX(16, UNAUTHENTICATED, "Unauthenticated", 401)

enum grpc_status
{
#define XX(num, name, string, http_eq) GRPC_STATUS_##name = num,
GRPC_STATUS_MAP(XX)
#undef XX
};

static inline const char* grpc_status_str(enum grpc_status s)
{
switch (s)
{
#define XX(num, name, string, http_eq) \
case GRPC_STATUS_##name: \
return string;
GRPC_STATUS_MAP(XX)
#undef XX
default:
return "<unknown>";
}
}

namespace ccf::grpc
{
int32_t status_to_code(const grpc_status& status)
static int32_t status_to_code(const grpc_status& status)
{
return static_cast<int32_t>(status);
}

protobuf::Status make_grpc_status(
static protobuf::Status make_grpc_status(
enum grpc_status status,
const std::optional<std::string>& msg = std::nullopt)
{
Expand All @@ -81,7 +40,7 @@ namespace ccf::grpc
return s;
}

protobuf::Status make_grpc_status_ok()
static protobuf::Status make_grpc_status_ok()
{
return make_grpc_status(GRPC_STATUS_OK);
}
Expand Down
27 changes: 6 additions & 21 deletions src/endpoints/grpc/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,6 @@

namespace ccf::grpc
{
static const http::HeaderMap default_response_headers = {
{http::headers::CONTENT_TYPE, http::headervalues::contenttype::GRPC}};

static constexpr auto TRAILER_STATUS = "grpc-status";
static constexpr auto TRAILER_MESSAGE = "grpc-message";

static http::HeaderKeyValue make_status_trailer(int32_t code)
{
return {TRAILER_STATUS, std::to_string(code)};
}

static http::HeaderKeyValue make_message_trailer(const std::string& msg)
{
return {TRAILER_MESSAGE, msg};
}

template <typename T>
struct SuccessResponse
{
Expand Down Expand Up @@ -62,28 +46,29 @@ namespace ccf::grpc
using GrpcAdapterEmptyResponse = GrpcAdapterResponse<EmptyResponse>;

template <typename T>
GrpcAdapterResponse<T> make_success(const T& t)
static GrpcAdapterResponse<T> make_success(const T& t)
{
return SuccessResponse(t, make_grpc_status_ok());
}

GrpcAdapterEmptyResponse make_success()
static GrpcAdapterEmptyResponse make_success()
{
return SuccessResponse(EmptyResponse{}, make_grpc_status_ok());
}

PendingResponse make_pending()
static PendingResponse make_pending()
{
return PendingResponse{};
}

ErrorResponse make_error(grpc_status code, const std::string& msg)
static ErrorResponse make_error(grpc_status code, const std::string& msg)
{
return ErrorResponse(make_grpc_status(code, msg));
}

template <typename T>
GrpcAdapterResponse<T> make_error(grpc_status code, const std::string& msg)
static GrpcAdapterResponse<T> make_error(
grpc_status code, const std::string& msg)
{
return ErrorResponse(make_grpc_status(code, msg));
}
Expand Down
5 changes: 3 additions & 2 deletions src/node/rpc/frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "common/configuration.h"
#include "consensus/aft/request.h"
#include "enclave/rpc_handler.h"
#include "endpoints/grpc/grpc_status.h"
#include "forwarder.h"
#include "http/http_jwt.h"
#include "kv/compacted_version_conflict.h"
Expand Down Expand Up @@ -271,7 +272,7 @@ namespace ccf
ctx->set_error(
HTTP_STATUS_UNAUTHORIZED,
ccf::errors::InvalidAuthenticationInfo,
"Invalid info",
"Invalid authentication credentials.",
error_details);
update_metrics(ctx);
}
Expand Down Expand Up @@ -481,7 +482,7 @@ namespace ccf
{
LOG_FAIL_FMT(
"Bad endpoint: During execution of {} {}, returned a non-pending "
"response but stole owneship of Tx object",
"response but stole ownership of Tx object",
ctx->get_request_verb().c_str(),
ctx->get_request_path());

Expand Down
Loading

0 comments on commit 5e0d4f3

Please sign in to comment.