Skip to content

Commit

Permalink
Unframed grpc service support richer error model (#4231)
Browse files Browse the repository at this point in the history
Motivation:

We want to add another Unframed Grpcerrorhandler to support richer grpc error information
[google errors](https://grpc.io/docs/guides/error/)

Modifications:

- add `ofRichJson()` method to `UnframedGrpcErrorHandler`.

Result:

- You can have a richer error model as described by [google error model](https://cloud.google.com/apis/design/errors#error_model) 

eg.  
- if you throw exceptions like 

 ```java
final ErrorInfo errorInfo = ErrorInfo.newBuilder()
                                     .setDomain("test")
                                     .setReason("Unknown Exception").build();
final com.google.rpc.Status status =
        com.google.rpc.Status.newBuilder()
                             .setCode(Code.UNKNOWN.getNumber())
                             .setMessage("Unknown Exceptions Test")
                             .addDetails(Any.pack(errorInfo))
                             .build();

responseObserver.onError(StatusProto.toStatusRuntimeException(status));
```
You can use this error handler by doing
``` java
sb.service(GrpcService.builder()   
                      .addService(grpcService)   
                      .unframedGrpcErrorHandler(UnframedGrpcErrorHandler.ofRichJson())   
                      .build());
```

You can get a response 

```
{
	"error": {
		"code": 500,
		"status": "UNKNOWN",
		"message": "Unknown Exceptions Test",
		"details": [{
			"@type": "type.googleapis.com/google.rpc.ErrorInfo",
			"reason": "Unknown Exception",
			"domain": "test"
		}]
	}
}
```
  • Loading branch information
natsumehu authored Aug 4, 2022
1 parent 03645e9 commit 7617ac4
Show file tree
Hide file tree
Showing 5 changed files with 589 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,16 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.server.grpc;

import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableMap;
package com.linecorp.armeria.server.grpc;

import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpData;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.ResponseHeaders;
import com.linecorp.armeria.common.annotation.UnstableApi;
import com.linecorp.armeria.common.grpc.protocol.GrpcHeaderNames;
import com.linecorp.armeria.common.logging.RequestLogAccess;
import com.linecorp.armeria.common.logging.RequestLogProperty;
import com.linecorp.armeria.common.util.Exceptions;
import com.linecorp.armeria.internal.common.util.TemporaryThreadLocals;
import com.linecorp.armeria.server.ServiceRequestContext;

import io.grpc.Status;
import io.grpc.Status.Code;

/**
* Error handler which maps a gRPC response to an {@link HttpResponse}.
Expand All @@ -47,7 +35,7 @@ public interface UnframedGrpcErrorHandler {
* Returns a plain text or json response based on the content type.
*/
static UnframedGrpcErrorHandler of() {
return of(UnframedGrpcStatusMappingFunction.of());
return UnframedGrpcErrorHandlers.of(UnframedGrpcStatusMappingFunction.of());
}

/**
Expand All @@ -57,26 +45,14 @@ static UnframedGrpcErrorHandler of() {
* to an {@link HttpStatus} code.
*/
static UnframedGrpcErrorHandler of(UnframedGrpcStatusMappingFunction statusMappingFunction) {
// Ensure that unframedGrpcStatusMappingFunction never returns null
// by falling back to the default.
final UnframedGrpcStatusMappingFunction mappingFunction =
requireNonNull(statusMappingFunction, "statusMappingFunction")
.orElse(UnframedGrpcStatusMappingFunction.of());
return (ctx, status, response) -> {
final MediaType grpcMediaType = response.contentType();
if (grpcMediaType != null && grpcMediaType.isJson()) {
return ofJson(mappingFunction).handle(ctx, status, response);
} else {
return ofPlainText(mappingFunction).handle(ctx, status, response);
}
};
return UnframedGrpcErrorHandlers.of(statusMappingFunction);
}

/**
* Returns a json response.
* Returns a JSON response.
*/
static UnframedGrpcErrorHandler ofJson() {
return ofJson(UnframedGrpcStatusMappingFunction.of());
return UnframedGrpcErrorHandlers.ofJson(UnframedGrpcStatusMappingFunction.of());
}

/**
Expand All @@ -86,44 +62,14 @@ static UnframedGrpcErrorHandler ofJson() {
* to an {@link HttpStatus} code.
*/
static UnframedGrpcErrorHandler ofJson(UnframedGrpcStatusMappingFunction statusMappingFunction) {
// Ensure that unframedGrpcStatusMappingFunction never returns null
// by falling back to the default.
final UnframedGrpcStatusMappingFunction mappingFunction =
requireNonNull(statusMappingFunction, "statusMappingFunction")
.orElse(UnframedGrpcStatusMappingFunction.of());
return (ctx, status, response) -> {
final Code grpcCode = status.getCode();
final String grpcMessage = status.getDescription();
final RequestLogAccess log = ctx.log();
final Throwable cause;
if (log.isAvailable(RequestLogProperty.RESPONSE_CAUSE)) {
cause = log.partial().responseCause();
} else {
cause = null;
}
final HttpStatus httpStatus = mappingFunction.apply(ctx, status, cause);
final ResponseHeaders responseHeaders = ResponseHeaders.builder(httpStatus)
.contentType(MediaType.JSON_UTF_8)
.addInt(GrpcHeaderNames.GRPC_STATUS,
grpcCode.value())
.build();
final ImmutableMap.Builder<String, String> messageBuilder = ImmutableMap.builder();
messageBuilder.put("grpc-code", grpcCode.name());
if (grpcMessage != null) {
messageBuilder.put("message", grpcMessage);
}
if (cause != null && ctx.config().verboseResponses()) {
messageBuilder.put("stack-trace", Exceptions.traceText(cause));
}
return HttpResponse.ofJson(responseHeaders, messageBuilder.build());
};
return UnframedGrpcErrorHandlers.ofJson(statusMappingFunction);
}

/**
* Returns a plain text response.
*/
static UnframedGrpcErrorHandler ofPlainText() {
return ofPlainText(UnframedGrpcStatusMappingFunction.of());
return UnframedGrpcErrorHandlers.ofPlaintext(UnframedGrpcStatusMappingFunction.of());
}

/**
Expand All @@ -133,41 +79,28 @@ static UnframedGrpcErrorHandler ofPlainText() {
* to an {@link HttpStatus} code.
*/
static UnframedGrpcErrorHandler ofPlainText(UnframedGrpcStatusMappingFunction statusMappingFunction) {
// Ensure that unframedGrpcStatusMappingFunction never returns null
// by falling back to the default.
final UnframedGrpcStatusMappingFunction mappingFunction =
requireNonNull(statusMappingFunction, "statusMappingFunction")
.orElse(UnframedGrpcStatusMappingFunction.of());
return (ctx, status, response) -> {
final Code grpcCode = status.getCode();
final RequestLogAccess log = ctx.log();
final Throwable cause;
if (log.isAvailable(RequestLogProperty.RESPONSE_CAUSE)) {
cause = log.partial().responseCause();
} else {
cause = null;
}
final HttpStatus httpStatus = mappingFunction.apply(ctx, status, cause);
final ResponseHeaders responseHeaders = ResponseHeaders.builder(httpStatus)
.contentType(MediaType.PLAIN_TEXT_UTF_8)
.addInt(GrpcHeaderNames.GRPC_STATUS,
grpcCode.value())
.build();
final HttpData content;
try (TemporaryThreadLocals ttl = TemporaryThreadLocals.acquire()) {
final StringBuilder msg = ttl.stringBuilder();
msg.append("grpc-code: ").append(grpcCode.name());
final String grpcMessage = status.getDescription();
if (grpcMessage != null) {
msg.append(", ").append(grpcMessage);
}
if (cause != null && ctx.config().verboseResponses()) {
msg.append("\nstack-trace:\n").append(Exceptions.traceText(cause));
}
content = HttpData.ofUtf8(msg);
}
return HttpResponse.of(responseHeaders, content);
};
return UnframedGrpcErrorHandlers.ofPlaintext(statusMappingFunction);
}

/**
* Returns a rich error JSON response based on Google APIs.
* See <a href="https://cloud.google.com/apis/design/errors#error_model">Google error model</a>
* for more information.
*/
static UnframedGrpcErrorHandler ofRichJson() {
return ofRichJson(UnframedGrpcStatusMappingFunction.of());
}

/**
* Returns a rich error JSON response based on Google APIs.
* See <a href="https://cloud.google.com/apis/design/errors#error_model">Google error model</a>
* for more information.
*
* @param statusMappingFunction The function which maps the {@link Throwable} or gRPC {@link Status} code
* to an {@link HttpStatus} code.
*/
static UnframedGrpcErrorHandler ofRichJson(UnframedGrpcStatusMappingFunction statusMappingFunction) {
return UnframedGrpcErrorHandlers.ofRichJson(statusMappingFunction);
}

/**
Expand Down
Loading

0 comments on commit 7617ac4

Please sign in to comment.