Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into refactor/grpc-status
Browse files Browse the repository at this point in the history
  • Loading branch information
jaeseung-bae committed Apr 17, 2024
2 parents d4929fe + 1d268aa commit 5cec1bf
Show file tree
Hide file tree
Showing 7 changed files with 727 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
@UnstableApi
public interface UnframedGrpcErrorHandler {

/**
* Returns a new {@link UnframedGrpcErrorHandlerBuilder}.
*/
@UnstableApi
static UnframedGrpcErrorHandlerBuilder builder() {
return new UnframedGrpcErrorHandlerBuilder();
}

/**
* Returns a plain text or json response based on the content type.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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 com.linecorp.armeria.server.grpc;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

import org.curioswitch.common.protobuf.json.MessageMarshaller;

import com.google.protobuf.Message;

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* Constructs a {@link UnframedGrpcErrorHandler} to handle unframed gRPC errors.
*/
@UnstableApi
public final class UnframedGrpcErrorHandlerBuilder {
private UnframedGrpcStatusMappingFunction statusMappingFunction = UnframedGrpcStatusMappingFunction.of();

@Nullable
private MessageMarshaller jsonMarshaller;

@Nullable
private List<Message> marshalledMessages;

@Nullable
private List<Class<? extends Message>> marshalledMessageTypes;

@Nullable
private Set<UnframedGrpcErrorResponseType> responseTypes;

UnframedGrpcErrorHandlerBuilder() {}

/**
* Sets a custom JSON marshaller to be used by the error handler.
*
* <p>This method allows the caller to specify a custom JSON marshaller
* for encoding the error responses. If messages or message types have
* already been registered, calling this method will result in an
* {@link IllegalStateException}. If nothing is specified,
* {@link UnframedGrpcErrorHandlers#ERROR_DETAILS_MARSHALLER} is used as
* default json marshaller.
*
* @param jsonMarshaller The custom JSON marshaller to use
*/
public UnframedGrpcErrorHandlerBuilder jsonMarshaller(MessageMarshaller jsonMarshaller) {
requireNonNull(jsonMarshaller, "jsonMarshaller");
checkState(marshalledMessages == null && marshalledMessageTypes == null,
"Cannot set a custom JSON marshaller because one or more Message instances or " +
"Message types have already been registered. To set a custom marshaller, " +
"ensure that no Message or Message type registrations have been made before " +
"calling this method."
);
this.jsonMarshaller = jsonMarshaller;
return this;
}

/**
* Specifies the status mapping function to be used by the error handler.
*
* <p>This function determines how gRPC statuses are mapped to HTTP statuses
* in the error response.
*
* @param statusMappingFunction The status mapping function
*/
public UnframedGrpcErrorHandlerBuilder statusMappingFunction(
UnframedGrpcStatusMappingFunction statusMappingFunction) {
this.statusMappingFunction = requireNonNull(statusMappingFunction, "statusMappingFunction");
return this;
}

/**
* Specifies the response types that the error handler will support.
*
* <p>This method allows specifying one or more response types (e.g., JSON, PLAINTEXT)
* that the error handler can produce. If nothing is specified or multiple types are specified, the actual
* response type is determined by the response's content type.
*
* @param responseTypes The response types to support
*/
public UnframedGrpcErrorHandlerBuilder responseTypes(UnframedGrpcErrorResponseType... responseTypes) {
requireNonNull(responseTypes, "responseTypes");

if (this.responseTypes == null) {
this.responseTypes = EnumSet.noneOf(UnframedGrpcErrorResponseType.class);
}
Collections.addAll(this.responseTypes, responseTypes);
return this;
}

/**
* Registers custom messages to be marshalled by the error handler.
*
* <p>This method registers specific message instances for custom error responses.
* If a custom JSON marshaller has already been set, calling this method will
* result in an {@link IllegalStateException}.
*
* @param messages The message instances to register
*/
public UnframedGrpcErrorHandlerBuilder registerMarshalledMessages(Message... messages) {
requireNonNull(messages, "messages");
checkState(jsonMarshaller == null,
"Cannot register custom messages because a custom JSON marshaller has already been set. " +
"Use the custom marshaller to register custom messages.");

if (marshalledMessages == null) {
marshalledMessages = new ArrayList<>();
}
Collections.addAll(marshalledMessages, messages);
return this;
}

/**
* Registers custom messages to be marshalled by the error handler.
*
* <p>This method allows registering message instances for custom error responses.
* If a custom JSON marshaller has already been set, calling this method will
* result in an {@link IllegalStateException}.
*
* @param messages The collection of messages to register
*/
public UnframedGrpcErrorHandlerBuilder registerMarshalledMessages(
Iterable<? extends Message> messages) {
requireNonNull(messages, "messages");
checkState(jsonMarshaller == null,
"Cannot register the collection of messages because a custom JSON marshaller has " +
"already been set. Use the custom marshaller to register custom messages.");

if (marshalledMessages == null) {
marshalledMessages = new ArrayList<>();
}
messages.forEach(marshalledMessages::add);
return this;
}

/**
* Registers custom message types to be marshalled by the error handler.
*
* <p>This method registers specific message types for custom error responses.
* If a custom JSON marshaller has already been set, calling this method will
* result in an {@link IllegalStateException}.
*
* @param messageTypes The message types to register
*/
@SafeVarargs
public final UnframedGrpcErrorHandlerBuilder registerMarshalledMessageTypes(
Class<? extends Message>... messageTypes) {
requireNonNull(messageTypes, "messageTypes");
checkState(jsonMarshaller == null,
"Cannot register custom messageTypes because a custom JSON marshaller has already been " +
"set. Use the custom marshaller to register custom message types.");

if (marshalledMessageTypes == null) {
marshalledMessageTypes = new ArrayList<>();
}
Collections.addAll(marshalledMessageTypes, messageTypes);
return this;
}

/**
* Registers custom message types to be marshalled by the error handler.
*
* <p>This method allows registering message instances for custom error responses.
* If a custom JSON marshaller has already been set, calling this method will
* result in an {@link IllegalStateException}.
*
* @param messageTypes The collection of message types to register
*/
public UnframedGrpcErrorHandlerBuilder registerMarshalledMessageTypes(
Iterable<? extends Class<? extends Message>> messageTypes) {
requireNonNull(messageTypes, "messageTypes");
checkState(jsonMarshaller == null,
"Cannot register the collection of messageTypes because a custom JSON marshaller has " +
"already been set. Use the custom marshaller to register custom message types.");

if (marshalledMessageTypes == null) {
marshalledMessageTypes = new ArrayList<>();
}
messageTypes.forEach(marshalledMessageTypes::add);
return this;
}

/**
* Returns a newly created {@link UnframedGrpcErrorHandler}.
*
* <p>This method constructs a new {@code UnframedGrpcErrorHandler} with the
* current configuration of this builder.
*/
public UnframedGrpcErrorHandler build() {
if (jsonMarshaller == null) {
jsonMarshaller = UnframedGrpcErrorHandlers.ERROR_DETAILS_MARSHALLER;
final MessageMarshaller.Builder builder = jsonMarshaller.toBuilder();

if (marshalledMessages != null) {
for (final Message message : marshalledMessages) {
builder.register(message);
}
}

if (marshalledMessageTypes != null) {
for (final Class<? extends Message> messageType : marshalledMessageTypes) {
builder.register(messageType);
}
}

jsonMarshaller = builder.build();
}

if (responseTypes == null) {
return UnframedGrpcErrorHandlers.of(statusMappingFunction, jsonMarshaller);
}

if (responseTypes.contains(UnframedGrpcErrorResponseType.JSON) &&
responseTypes.contains(UnframedGrpcErrorResponseType.PLAINTEXT)) {
return UnframedGrpcErrorHandlers.of(statusMappingFunction, jsonMarshaller);
}

if (responseTypes.contains(UnframedGrpcErrorResponseType.JSON)) {
return UnframedGrpcErrorHandlers.ofJson(statusMappingFunction, jsonMarshaller);
}

if (responseTypes.contains(UnframedGrpcErrorResponseType.PLAINTEXT)) {
return UnframedGrpcErrorHandlers.ofPlaintext(statusMappingFunction);
}

return UnframedGrpcErrorHandlers.of(statusMappingFunction, jsonMarshaller);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ final class UnframedGrpcErrorHandlers {

private static final Logger logger = LoggerFactory.getLogger(UnframedGrpcErrorHandlers.class);

// XXX(ikhoon): Support custom JSON marshaller?
private static final MessageMarshaller ERROR_DETAILS_MARSHALLER =
static final MessageMarshaller ERROR_DETAILS_MARSHALLER =
MessageMarshaller.builder()
.omittingInsignificantWhitespace(true)
.register(RetryInfo.getDefaultInstance())
Expand All @@ -93,11 +92,16 @@ final class UnframedGrpcErrorHandlers {
* to an {@link HttpStatus} code.
*/
static UnframedGrpcErrorHandler of(UnframedGrpcStatusMappingFunction statusMappingFunction) {
return of(statusMappingFunction, ERROR_DETAILS_MARSHALLER);
}

static UnframedGrpcErrorHandler of(
UnframedGrpcStatusMappingFunction statusMappingFunction, MessageMarshaller jsonMarshaller) {
final UnframedGrpcStatusMappingFunction mappingFunction = withDefault(statusMappingFunction);
return (ctx, status, response) -> {
final MediaType grpcMediaType = response.contentType();
if (grpcMediaType != null && grpcMediaType.isJson()) {
return ofJson(mappingFunction).handle(ctx, status, response);
return ofJson(mappingFunction, jsonMarshaller).handle(ctx, status, response);
} else {
return ofPlaintext(mappingFunction).handle(ctx, status, response);
}
Expand All @@ -110,7 +114,8 @@ static UnframedGrpcErrorHandler of(UnframedGrpcStatusMappingFunction statusMappi
* @param statusMappingFunction The function which maps the {@link Throwable} or gRPC {@link Status} code
* to an {@link HttpStatus} code.
*/
static UnframedGrpcErrorHandler ofJson(UnframedGrpcStatusMappingFunction statusMappingFunction) {
static UnframedGrpcErrorHandler ofJson(
UnframedGrpcStatusMappingFunction statusMappingFunction, MessageMarshaller jsonMarshaller) {
final UnframedGrpcStatusMappingFunction mappingFunction = withDefault(statusMappingFunction);
return (ctx, status, response) -> {
final ByteBuf buffer = ctx.alloc().buffer();
Expand Down Expand Up @@ -148,7 +153,7 @@ static UnframedGrpcErrorHandler ofJson(UnframedGrpcStatusMappingFunction statusM
}
if (rpcStatus != null) {
jsonGenerator.writeFieldName("details");
writeErrorDetails(rpcStatus.getDetailsList(), jsonGenerator);
writeErrorDetails(rpcStatus.getDetailsList(), jsonGenerator, jsonMarshaller);
}
}
jsonGenerator.writeEndObject();
Expand All @@ -169,6 +174,10 @@ static UnframedGrpcErrorHandler ofJson(UnframedGrpcStatusMappingFunction statusM
};
}

static UnframedGrpcErrorHandler ofJson(UnframedGrpcStatusMappingFunction statusMappingFunction) {
return ofJson(statusMappingFunction, ERROR_DETAILS_MARSHALLER);
}

/**
* Returns a plaintext response.
*
Expand Down Expand Up @@ -217,11 +226,12 @@ private static UnframedGrpcStatusMappingFunction withDefault(
}

@VisibleForTesting
static void writeErrorDetails(List<Any> details, JsonGenerator jsonGenerator) throws IOException {
static void writeErrorDetails(List<Any> details, JsonGenerator jsonGenerator,
MessageMarshaller jsonMarshaller) throws IOException {
jsonGenerator.writeStartArray();
for (Any detail : details) {
try {
ERROR_DETAILS_MARSHALLER.writeValue(detail, jsonGenerator);
jsonMarshaller.writeValue(detail, jsonGenerator);
} catch (IOException e) {
logger.warn("Unexpected exception while writing an error detail to JSON. detail: {}",
detail, e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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 com.linecorp.armeria.server.grpc;

import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* The types of responses that can be sent when handling errors in an unframed gRPC service.
*
* <p>When multiple {@code UnframedGrpcErrorResponseType} values are selected, the actual response type
* is determined by the response's {@code contentType}.
*/
@UnstableApi
public enum UnframedGrpcErrorResponseType {
/**
* The error response will be formatted as a JSON object.
*/
JSON,

/**
* The error response will be sent as plain text.
*/
PLAINTEXT,
}

0 comments on commit 5cec1bf

Please sign in to comment.