Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserialize endpoint errors #2443

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
6 changes: 6 additions & 0 deletions .palantir/revapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,9 @@ acceptedBreaks:
new: "method com.palantir.dialogue.clients.DialogueClients.StickyChannelSession\
\ com.palantir.dialogue.clients.DialogueClients.StickyChannelFactory2::session()"
justification: "interface for consumption, not extension"
"4.6.0":
com.palantir.dialogue:dialogue-target:
- code: "java.method.addedToInterface"
new: "method <T> com.palantir.dialogue.Deserializer<T> com.palantir.dialogue.BodySerDe::deserializer(com.palantir.dialogue.DeserializerArgs<T>)"
justification: "Adding a new method to create deserializers in support of endpoint\
\ associated error deserialization"
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.palantir.dialogue.BinaryRequestBody;
import com.palantir.dialogue.BodySerDe;
import com.palantir.dialogue.Deserializer;
import com.palantir.dialogue.DeserializerArgs;
import com.palantir.dialogue.RequestBody;
import com.palantir.dialogue.Response;
import com.palantir.dialogue.Serializer;
Expand All @@ -42,6 +43,7 @@
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
Expand All @@ -58,7 +60,8 @@ final class ConjureBodySerDe implements BodySerDe {
private final Deserializer<Optional<InputStream>> optionalBinaryInputStreamDeserializer;
private final Deserializer<Void> emptyBodyDeserializer;
private final LoadingCache<Type, Serializer<?>> serializers;
private final LoadingCache<Type, Deserializer<?>> deserializers;
private final LoadingCache<Type, EncodingDeserializerForEndpointRegistry<?>> deserializers;
private final EmptyContainerDeserializer emptyContainerDeserializer;
mpritham marked this conversation as resolved.
Show resolved Hide resolved

/**
* Selects the first (based on input order) of the provided encodings that
Expand All @@ -67,31 +70,46 @@ final class ConjureBodySerDe implements BodySerDe {
*/
ConjureBodySerDe(
List<WeightedEncoding> rawEncodings,
ErrorDecoder errorDecoder,
EmptyContainerDeserializer emptyContainerDeserializer,
CaffeineSpec cacheSpec) {
List<WeightedEncoding> encodings = decorateEncodings(rawEncodings);
this.encodingsSortedByWeight = sortByWeight(encodings);
Preconditions.checkArgument(encodings.size() > 0, "At least one Encoding is required");
this.defaultEncoding = encodings.get(0).encoding();
this.binaryInputStreamDeserializer = new EncodingDeserializerRegistry<>(
this.emptyContainerDeserializer = emptyContainerDeserializer;
this.binaryInputStreamDeserializer = new EncodingDeserializerForEndpointRegistry<>(
ImmutableList.of(BinaryEncoding.INSTANCE),
errorDecoder,
emptyContainerDeserializer,
BinaryEncoding.MARKER);
this.optionalBinaryInputStreamDeserializer = new EncodingDeserializerRegistry<>(
BinaryEncoding.MARKER,
DeserializerArgs.<InputStream>builder()
.baseType(BinaryEncoding.MARKER)
.success(BinaryEncoding.MARKER)
.build());
this.optionalBinaryInputStreamDeserializer = new EncodingDeserializerForEndpointRegistry<>(
ImmutableList.of(BinaryEncoding.INSTANCE),
errorDecoder,
emptyContainerDeserializer,
BinaryEncoding.OPTIONAL_MARKER);
this.emptyBodyDeserializer = new EmptyBodyDeserializer(errorDecoder);
BinaryEncoding.OPTIONAL_MARKER,
DeserializerArgs.<Optional<InputStream>>builder()
.baseType(BinaryEncoding.OPTIONAL_MARKER)
.success(BinaryEncoding.OPTIONAL_MARKER)
.build());
this.emptyBodyDeserializer = new EmptyBodyDeserializer(new EndpointErrorDecoder<>(Collections.emptyMap()));
// Class unloading: Not supported, Jackson keeps strong references to the types
// it sees: https://github.com/FasterXML/jackson-databind/issues/489
this.serializers = Caffeine.from(cacheSpec)
.build(type -> new EncodingSerializerRegistry<>(defaultEncoding, TypeMarker.of(type)));
this.deserializers = Caffeine.from(cacheSpec)
.build(type -> new EncodingDeserializerRegistry<>(
encodingsSortedByWeight, errorDecoder, emptyContainerDeserializer, TypeMarker.of(type)));
this.deserializers = Caffeine.from(cacheSpec).build(type -> buildCacheEntry(TypeMarker.of(type)));
}

private <T> EncodingDeserializerForEndpointRegistry<?> buildCacheEntry(TypeMarker<T> typeMarker) {
return new EncodingDeserializerForEndpointRegistry<>(
encodingsSortedByWeight,
emptyContainerDeserializer,
typeMarker,
DeserializerArgs.<T>builder()
.baseType(typeMarker)
.success(typeMarker)
.build());
}

private static List<WeightedEncoding> decorateEncodings(List<WeightedEncoding> input) {
Expand Down Expand Up @@ -122,6 +140,16 @@ public <T> Deserializer<T> deserializer(TypeMarker<T> token) {
return (Deserializer<T>) deserializers.get(token.getType());
}

@Override
@SuppressWarnings("unchecked")
public <T> Deserializer<T> deserializer(DeserializerArgs<T> deserializerArgs) {
return new EncodingDeserializerForEndpointRegistry<>(
encodingsSortedByWeight,
emptyContainerDeserializer,
(TypeMarker<T>) deserializerArgs.baseType(),
deserializerArgs);
}

@Override
public Deserializer<Void> emptyBodyDeserializer() {
return emptyBodyDeserializer;
Expand Down Expand Up @@ -216,37 +244,42 @@ private static final class EncodingSerializerContainer<T> {
}
}

private static final class EncodingDeserializerRegistry<T> implements Deserializer<T> {

private static final SafeLogger log = SafeLoggerFactory.get(EncodingDeserializerRegistry.class);
private final ImmutableList<EncodingDeserializerContainer<T>> encodings;
private final ErrorDecoder errorDecoder;
private static final class EncodingDeserializerForEndpointRegistry<T> implements Deserializer<T> {
private static final SafeLogger log = SafeLoggerFactory.get(EncodingDeserializerForEndpointRegistry.class);
private final ImmutableList<EncodingDeserializerContainer<? extends T>> encodings;
private final EndpointErrorDecoder<T> endpointErrorDecoder;
private final Optional<String> acceptValue;
private final Supplier<Optional<T>> emptyInstance;
private final TypeMarker<T> token;

EncodingDeserializerRegistry(
List<Encoding> encodings,
ErrorDecoder errorDecoder,
EncodingDeserializerForEndpointRegistry(
List<Encoding> encodingsSortedByWeight,
EmptyContainerDeserializer empty,
TypeMarker<T> token) {
this.encodings = encodings.stream()
.map(encoding -> new EncodingDeserializerContainer<>(encoding, token))
TypeMarker<T> token,
DeserializerArgs<T> deserializersForEndpoint) {
this.encodings = encodingsSortedByWeight.stream()
.map(encoding ->
new EncodingDeserializerContainer<>(encoding, deserializersForEndpoint.successType()))
.collect(ImmutableList.toImmutableList());
this.errorDecoder = errorDecoder;
this.endpointErrorDecoder = new EndpointErrorDecoder<>(
deserializersForEndpoint.errorNameToTypeMarker(),
encodingsSortedByWeight.stream()
.filter(encoding -> encoding.supportsContentType("application/json"))
.findFirst());
this.token = token;
this.emptyInstance = Suppliers.memoize(() -> empty.tryGetEmptyInstance(token));
// Encodings are applied to the accept header in the order of preference based on the provided list.
this.acceptValue =
Optional.of(encodings.stream().map(Encoding::getContentType).collect(Collectors.joining(", ")));
this.acceptValue = Optional.of(encodingsSortedByWeight.stream()
.map(Encoding::getContentType)
.collect(Collectors.joining(", ")));
}

@Override
public T deserialize(Response response) {
boolean closeResponse = true;
try {
if (errorDecoder.isError(response)) {
throw errorDecoder.decode(response);
if (endpointErrorDecoder.isError(response)) {
return endpointErrorDecoder.decode(response);
} else if (response.code() == 204) {
// TODO(dfox): what if we get a 204 for a non-optional type???
// TODO(dfox): support http200 & body=null
Expand All @@ -265,7 +298,7 @@ public T deserialize(Response response) {
"Response is missing Content-Type header",
SafeArg.of("received", response.headers().keySet()));
}
Encoding.Deserializer<T> deserializer = getResponseDeserializer(contentType.get());
Encoding.Deserializer<? extends T> deserializer = getResponseDeserializer(contentType.get());
T deserialized = deserializer.deserialize(response.body());
// deserializer has taken on responsibility for closing the response body
closeResponse = false;
Expand All @@ -291,9 +324,9 @@ public Optional<String> accepts() {
/** Returns the {@link EncodingDeserializerContainer} to use to deserialize the request body. */
@SuppressWarnings("ForLoopReplaceableByForEach")
// performance sensitive code avoids iterator allocation
Encoding.Deserializer<T> getResponseDeserializer(String contentType) {
Encoding.Deserializer<? extends T> getResponseDeserializer(String contentType) {
for (int i = 0; i < encodings.size(); i++) {
EncodingDeserializerContainer<T> container = encodings.get(i);
EncodingDeserializerContainer<? extends T> container = encodings.get(i);
if (container.encoding.supportsContentType(contentType)) {
return container.deserializer;
}
Expand Down Expand Up @@ -337,19 +370,19 @@ public String toString() {
}

private static final class EmptyBodyDeserializer implements Deserializer<Void> {
private final ErrorDecoder errorDecoder;
private final EndpointErrorDecoder<?> endpointErrorDecoder;

EmptyBodyDeserializer(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
EmptyBodyDeserializer(EndpointErrorDecoder<?> endpointErrorDecoder) {
this.endpointErrorDecoder = endpointErrorDecoder;
}

@Override
@SuppressWarnings("NullAway") // empty body is a special case
public Void deserialize(Response response) {
// We should not fail if a server that previously returned nothing starts returning a response
try (Response unused = response) {
if (errorDecoder.isError(response)) {
throw errorDecoder.decode(response);
if (endpointErrorDecoder.isError(response)) {
endpointErrorDecoder.decode(response);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public final class DefaultConjureRuntime implements ConjureRuntime {
private DefaultConjureRuntime(Builder builder) {
this.bodySerDe = new ConjureBodySerDe(
builder.encodings.isEmpty() ? DEFAULT_ENCODINGS : builder.encodings,
ErrorDecoder.INSTANCE,
Encodings.emptyContainerDeserializer(),
DEFAULT_SERDE_CACHE_SPEC);
}
Expand Down
Loading