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

Decode errors during deserialization #113

Merged
merged 4 commits into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

package com.palantir.conjure.java.dialogue.serde;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.palantir.dialogue.BinaryRequestBody;
import com.palantir.dialogue.BodySerDe;
import com.palantir.dialogue.Deserializer;
import com.palantir.dialogue.ErrorDecoder;
import com.palantir.dialogue.Headers;
import com.palantir.dialogue.RequestBody;
import com.palantir.dialogue.Response;
Expand All @@ -42,6 +44,7 @@ final class ConjureBodySerDe implements BodySerDe {
private static final String BINARY_CONTENT_TYPE = "application/octet-stream";

private final List<Encoding> encodings;
private final ErrorDecoder errorDecoder;
private final Encoding defaultEncoding;

/**
Expand All @@ -50,8 +53,15 @@ final class ConjureBodySerDe implements BodySerDe {
* by a given request, or the first serializer if no such serializer can be found.
*/
ConjureBodySerDe(List<Encoding> encodings) {
// TODO(jellis): consider supporting cbor encoded errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course

this(encodings, DefaultErrorDecoder.INSTANCE);
}

@VisibleForTesting
ConjureBodySerDe(List<Encoding> encodings, ErrorDecoder errorDecoder) {
// Defensive copy
this.encodings = ImmutableList.copyOf(encodings);
this.errorDecoder = errorDecoder;
Preconditions.checkArgument(encodings.size() > 0, "At least one Encoding is required");
this.defaultEncoding = encodings.get(0);
}
Expand All @@ -63,7 +73,7 @@ public <T> Serializer<T> serializer(TypeMarker<T> token) {

@Override
public <T> Deserializer<T> deserializer(TypeMarker<T> token) {
return new EncodingDeserializerRegistry<>(encodings, token);
return new EncodingDeserializerRegistry<>(encodings, errorDecoder, token);
}

@Override
Expand Down Expand Up @@ -168,15 +178,21 @@ private static final class EncodingSerializerContainer<T> {
private static final class EncodingDeserializerRegistry<T> implements Deserializer<T> {

private final List<EncodingDeserializerContainer<T>> encodings;
private final ErrorDecoder errorDecoder;

EncodingDeserializerRegistry(List<Encoding> encodings, TypeMarker<T> token) {
EncodingDeserializerRegistry(List<Encoding> encodings, ErrorDecoder errorDecoder, TypeMarker<T> token) {
this.encodings = encodings.stream()
.map(encoding -> new EncodingDeserializerContainer<>(encoding, token))
.collect(ImmutableList.toImmutableList());
this.errorDecoder = errorDecoder;
}

@Override
public T deserialize(Response response) {
if (errorDecoder.isError(response)) {
throw errorDecoder.decode(response);
}

Optional<String> contentType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);
EncodingDeserializerContainer<T> container = getResponseDeserializer(contentType);
return container.deserializer.deserialize(response.body());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@
package com.palantir.conjure.java.dialogue.serde;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.palantir.conjure.java.api.errors.ErrorType;
import com.palantir.conjure.java.api.errors.RemoteException;
import com.palantir.conjure.java.api.errors.SerializableError;
import com.palantir.conjure.java.api.errors.ServiceException;
import com.palantir.dialogue.BodySerDe;
import com.palantir.dialogue.ErrorDecoder;
import com.palantir.dialogue.RequestBody;
import com.palantir.dialogue.Response;
import com.palantir.dialogue.TypeMarker;
Expand All @@ -35,11 +42,17 @@
import java.util.Map;
import javax.ws.rs.core.HttpHeaders;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class ConjureBodySerDeTest {

private static final TypeMarker<String> TYPE = new TypeMarker<String>() {};

@Mock private ErrorDecoder errorDecoder;

@Test
public void testRequestContentType() throws IOException {
Encoding json = new StubEncoding("application/json");
Expand Down Expand Up @@ -103,6 +116,23 @@ public void testResponseUnknownContentType() throws IOException {
assertThat(body.contentType()).isEqualTo(json.getContentType());
}

@Test
public void testErrorsDecoded() {
TestResponse response = new TestResponse();
// response.contentType();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh?

response.code = 400;

ServiceException serviceException = new ServiceException(ErrorType.INVALID_ARGUMENT);
SerializableError serialized = SerializableError.forException(serviceException);
when(errorDecoder.isError(response)).thenReturn(true);
when(errorDecoder.decode(response)).thenReturn(new RemoteException(serialized, 400));

BodySerDe serializers = new ConjureBodySerDe(ImmutableList.of(new StubEncoding("text/plain")), errorDecoder);

assertThatExceptionOfType(RemoteException.class)
.isThrownBy(() -> serializers.deserializer(TYPE).deserialize(response));
}

/** Deserializes requests as the configured content type. */
public static final class StubEncoding implements Encoding {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ private static String createServiceException(ServiceException exception) {
@Test
public void extractsRemoteExceptionForAllErrorCodes() {
for (int code : ImmutableList.of(300, 400, 404, 500)) {
RemoteException exception = decode("application/json", code, SERIALIZED_EXCEPTION);
Response response = response(code, "application/json", SERIALIZED_EXCEPTION);
assertThat(decoder.isError(response)).isTrue();

RemoteException exception = decoder.decode(response);
assertThat(exception.getCause()).isNull();
assertThat(exception.getStatus()).isEqualTo(code);
assertThat(exception.getError().errorCode()).isEqualTo(ErrorType.FAILED_PRECONDITION.code().name());
Expand All @@ -89,21 +92,21 @@ public void testSpecificException() {

@Test
public void cannotDecodeNonJsonMediaTypes() {
assertThatThrownBy(() -> decode("text/plain", 500, SERIALIZED_EXCEPTION))
assertThatThrownBy(() -> decoder.decode(response(500, "text/plain", SERIALIZED_EXCEPTION)))
.isInstanceOf(SafeRuntimeException.class)
.hasMessage("Failed to interpret response body as SerializableError: {code=500}");
}

@Test
public void doesNotHandleUnparseableBody() {
assertThatThrownBy(() -> decode("application/json/", 500, "not json"))
assertThatThrownBy(() -> decoder.decode(response(500, "application/json/", "not json")))
.isInstanceOf(SafeRuntimeException.class)
.hasMessageStartingWith("Failed to interpret response body as SerializableError:");
}

@Test
public void doesNotHandleNullBody() {
assertThatThrownBy(() -> decode("application/json", 500, null))
assertThatThrownBy(() -> decoder.decode(response(500, "application/json", null)))
.isInstanceOf(SafeRuntimeException.class)
.hasMessageStartingWith(
"Failed to deserialize response body as JSON, could not deserialize SerializableError:");
Expand All @@ -127,11 +130,7 @@ private static RemoteException encodeAndDecode(Exception exception) {
// ? ((WebApplicationException) exception).getResponse().getStatus()
// : 400;
int status = 400;
return decode("application/json", status, json);
}

private static RemoteException decode(String contentType, int status, @CheckForNull String body) {
return decoder.decode(response(status, contentType, body));
return decoder.decode(response(status, "application/json", json));
}

private static Response response(int code, String mediaType, @CheckForNull String body) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,9 @@
* Response} does not adhere to an expected format.
*/
public interface ErrorDecoder {
default boolean isError(Response response) {
return 300 <= response.code() && response.code() <= 599;
}

RemoteException decode(Response response);
}