diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Encodings.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Encodings.java index ef85170d0..7c8f3405d 100644 --- a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Encodings.java +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Encodings.java @@ -28,7 +28,6 @@ import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; -import com.palantir.logsafe.exceptions.SafeIoException; import java.io.IOException; // TODO(rfink): Consider async Jackson, see @@ -72,7 +71,15 @@ public final Serializer serializer(TypeMarker type) { ObjectWriter writer = mapper.writerFor(mapper.constructType(type.getType())); return (value, output) -> { Preconditions.checkNotNull(value, "cannot serialize null value"); - writer.writeValue(output, value); + try { + writer.writeValue(output, value); + } catch (IOException e) { + throw FrameworkException.ioFailure( + "Failed to serialize and write response", + e, + SafeArg.of("contentType", getContentType()), + SafeArg.of("type", type)); + } }; } @@ -109,7 +116,7 @@ public final Deserializer deserializer(TypeMarker type) { SafeArg.of("contentType", getContentType()), SafeArg.of("type", type)); } catch (IOException e) { - throw new SafeIoException( + throw FrameworkException.ioFailure( "Failed to deserialize request", e, SafeArg.of("contentType", getContentType()), diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/FrameworkException.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/FrameworkException.java index acff2990c..1f5fdddb0 100644 --- a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/FrameworkException.java +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/FrameworkException.java @@ -19,10 +19,12 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CompileTimeConstant; import com.palantir.conjure.java.api.errors.ErrorType; +import com.palantir.conjure.java.api.errors.ErrorType.Code; import com.palantir.logsafe.Arg; import com.palantir.logsafe.SafeLoggable; import io.undertow.util.StatusCodes; import java.util.List; +import javax.annotation.Nullable; /** Internal type to signal a conjure protocol-level failure with a specific response code. */ final class FrameworkException extends RuntimeException implements SafeLoggable { @@ -31,6 +33,7 @@ final class FrameworkException extends RuntimeException implements SafeLoggable ErrorType.create(ErrorType.Code.INVALID_ARGUMENT, "Conjure:UnprocessableEntity"); private static final ErrorType UNSUPPORTED_MEDIA_TYPE = ErrorType.create(ErrorType.Code.INVALID_ARGUMENT, "Conjure:UnsupportedMediaType"); + private static final ErrorType IO_ERROR = ErrorType.create(Code.CUSTOM_CLIENT, "Conjure:Io"); private final String logMessage; private final List> arguments; @@ -54,6 +57,11 @@ static FrameworkException unsupportedMediaType(@CompileTimeConstant String messa return new FrameworkException(message, UNSUPPORTED_MEDIA_TYPE, StatusCodes.UNSUPPORTED_MEDIA_TYPE, null, args); } + static FrameworkException ioFailure( + @CompileTimeConstant String message, @Nullable Throwable cause, Arg... args) { + return new FrameworkException(message, IO_ERROR, IO_ERROR.httpErrorCode(), cause, args); + } + @Override public String getLogMessage() { return logMessage; diff --git a/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/EncodingsTest.java b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/EncodingsTest.java index 116ba7ddd..48210f214 100644 --- a/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/EncodingsTest.java +++ b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/EncodingsTest.java @@ -203,6 +203,51 @@ void smile_supportsContentType() { assertThat(smile.supportsContentType("application/unknown")).isFalse(); } + @Test + void deserializeIoExceptionIsClientError() { + assertThatThrownBy(() -> deserialize(new ThrowingInputStream(), new TypeMarker() {})) + .isInstanceOf(FrameworkException.class) + .hasMessageContaining("Failed to deserialize") + .matches(exception -> ((FrameworkException) exception).getStatusCode() == 400, "Expected 400 status"); + } + + @Test + void serializeIoExceptionIsClientError() { + assertThatThrownBy(() -> serialize("value", new ThrowingOutputStream())) + .isInstanceOf(FrameworkException.class) + .hasMessageContaining("Failed to serialize") + .matches(exception -> ((FrameworkException) exception).getStatusCode() == 400, "Expected 400 status"); + } + + private static final class ThrowingInputStream extends InputStream { + + private static IOException fail() { + return new IOException("expected"); + } + + @Override + public int read() throws IOException { + throw fail(); + } + + @Override + public int read(byte[] _buffer, int _off, int _len) throws IOException { + throw fail(); + } + } + + private static final class ThrowingOutputStream extends OutputStream { + + private static IOException fail() { + return new IOException("expected"); + } + + @Override + public void write(int _value) throws IOException { + throw fail(); + } + } + private static InputStream asStream(String data) { return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); }