Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -0,0 +1,23 @@
package io.temporal.common.converter;

import io.temporal.failure.ApplicationFailure;

/** Factory for failures raised when converting a value that violates its payload schema. */
public final class PayloadValidationException {
private static final String MESSAGE = "Payload validation failed";
private static final String TYPE = "PayloadValidationError";

private PayloadValidationException() {}

/**
* Creates a non-retryable failure containing payload validation details.
*
* <p>The details are stored as a single value and serialized by the configured {@link
* DataConverter}.
*
* @param details payload validation details
*/
public static ApplicationFailure newPayloadValidationException(Object details) {
return ApplicationFailure.newNonRetryableFailure(MESSAGE, TYPE, details);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.temporal.common.converter;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import com.google.common.reflect.TypeToken;
import io.temporal.api.failure.v1.Failure;
import io.temporal.failure.ApplicationFailure;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;

public class PayloadValidationExceptionTest {
@Test
public void
newPayloadValidationExceptionReturnsNonRetryableApplicationFailureWithEncodedDetails() {
List<Map<String, String>> details =
Collections.singletonList(Collections.singletonMap("path", "$.customer.contact.email"));

ApplicationFailure applicationFailure =
PayloadValidationException.newPayloadValidationException(details);

assertEquals("PayloadValidationError", applicationFailure.getType());
assertTrue(applicationFailure.isNonRetryable());
assertEquals("Payload validation failed", applicationFailure.getOriginalMessage());
assertEquals(1, applicationFailure.getDetails().getSize());

DataConverter dataConverter = DefaultDataConverter.STANDARD_INSTANCE;
Failure encodedFailure = dataConverter.exceptionToFailure(applicationFailure);
assertEquals(1, encodedFailure.getApplicationFailureInfo().getDetails().getPayloadsCount());

ApplicationFailure decodedFailure =
(ApplicationFailure) dataConverter.failureToException(encodedFailure);
TypeToken<List<Map<String, String>>> detailsType =
new TypeToken<List<Map<String, String>>>() {};
assertEquals(details, decodedFailure.getDetails().get(0, List.class, detailsType.getType()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.temporal.common.converter.DataConverterException;
import io.temporal.common.converter.DefaultDataConverter;
import io.temporal.common.converter.EncodedValuesTest;
import io.temporal.common.converter.PayloadValidationException;
import io.temporal.failure.ApplicationFailure;
import io.temporal.payload.codec.PayloadCodecException;
import java.lang.reflect.GenericArrayType;
Expand Down Expand Up @@ -128,10 +129,9 @@ public void testDeserializeApplicationFailureIsPropagatedAsIs() {
@Test
public void testDeserializeNonRetryablePayloadValidationErrorIsNonRetryableBadRequest() {
// The converter understood the input and rejected it, which makes this the caller's fault.
RuntimeException cause = new RuntimeException("field 'name' must not be empty");
ApplicationFailure original =
ApplicationFailure.newNonRetryableFailureWithCause(
"invalid input", PayloadSerializer.PAYLOAD_VALIDATION_ERROR_TYPE, cause);
PayloadValidationException.newPayloadValidationException(
Collections.singletonList(Collections.singletonMap("name", "must not be empty")));
PayloadSerializer serializer = failingSerializer(null, original);
PayloadSerializer.Content content = payloadSerializer.serialize("test");

Expand All @@ -149,8 +149,8 @@ public void testDeserializeNonRetryablePayloadValidationErrorIsNonRetryableBadRe
Assert.assertSame(original, causeFailure);
Assert.assertEquals(PayloadSerializer.PAYLOAD_VALIDATION_ERROR_TYPE, causeFailure.getType());
Assert.assertTrue(causeFailure.isNonRetryable());
Assert.assertEquals("invalid input", causeFailure.getOriginalMessage());
Assert.assertSame(cause, causeFailure.getCause());
Assert.assertEquals("Payload validation failed", causeFailure.getOriginalMessage());
Assert.assertEquals(1, causeFailure.getDetails().getSize());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.temporal.client.WorkflowFailedException;
import io.temporal.common.converter.DataConverter;
import io.temporal.common.converter.DefaultDataConverter;
import io.temporal.common.converter.PayloadValidationException;
import io.temporal.failure.ApplicationFailure;
import io.temporal.failure.NexusOperationFailure;
import io.temporal.failure.TimeoutFailure;
Expand All @@ -22,6 +23,7 @@
import io.temporal.workflow.shared.TestWorkflows.TestWorkflow1;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
Expand All @@ -45,7 +47,6 @@ public class OperationInputDeserializationErrorPropagationTest {
"non-retryable-payload-validation-error";
private static final String RETRYABLE_PAYLOAD_VALIDATION_ERROR =
"retryable-payload-validation-error";
private static final String PAYLOAD_VALIDATION_ERROR_TYPE = "PayloadValidationError";
private static final String CODEC_FAILURE = "codec-failure";

private static final AtomicInteger deserializeAttempts = new AtomicInteger();
Expand Down Expand Up @@ -145,7 +146,7 @@ public void nonRetryablePayloadValidationErrorBecomesNonRetryableBadRequest() {
Assert.assertEquals("PayloadValidationError", ((ApplicationFailure) cause).getType());
Assert.assertTrue(
"expected the converter's message on the cause, got " + cause.getMessage(),
cause.getMessage().contains("intentional failure"));
cause.getMessage().contains("Payload validation failed"));

Assert.assertEquals(1, deserializeAttempts.get());
Assert.assertEquals(0, operationInvocations.get());
Expand Down Expand Up @@ -288,11 +289,10 @@ private static RuntimeException failureFor(String mode) {
case RETRYABLE_APPLICATION_FAILURE:
return ApplicationFailure.newFailure("intentional failure", "TestFailure");
case NON_RETRYABLE_PAYLOAD_VALIDATION_ERROR:
return ApplicationFailure.newNonRetryableFailure(
"intentional failure", PAYLOAD_VALIDATION_ERROR_TYPE);
return PayloadValidationException.newPayloadValidationException(
Collections.singletonList("intentional validation failure"));
case RETRYABLE_PAYLOAD_VALIDATION_ERROR:
return ApplicationFailure.newFailure(
"intentional failure", PAYLOAD_VALIDATION_ERROR_TYPE);
return ApplicationFailure.newFailure("intentional failure", "PayloadValidationError");
case CODEC_FAILURE:
return new PayloadCodecException("intentional failure");
default:
Expand Down
Loading