From 94438aa5d15c7180768feb21e11bb4fa5190a476 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Wed, 31 Jul 2024 19:39:30 +1000 Subject: [PATCH 1/2] support schema for oneOf electra and phase0 Signed-off-by: Gabriel Fukushima --- .../beacon/MilestoneDependentTypesUtil.java | 18 +++ .../v2/beacon/PostAttesterSlashingV2.java | 106 +++++++++++------- 2 files changed, 86 insertions(+), 38 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/MilestoneDependentTypesUtil.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/MilestoneDependentTypesUtil.java index ebf54a3613f..11e4065577a 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/MilestoneDependentTypesUtil.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/MilestoneDependentTypesUtil.java @@ -13,8 +13,11 @@ package tech.pegasys.teku.beaconrestapi.handlers.v1.beacon; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; + import com.fasterxml.jackson.core.JsonProcessingException; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.BiPredicate; import java.util.function.Function; @@ -93,6 +96,21 @@ public static DeserializableTypeDefinition slot .getJsonTypeDefinition(); } + public static DeserializableTypeDefinition headerBasedSelector( + final Map headers, + final SchemaDefinitionCache schemaDefinitionCache, + final Function> getSchema) { + try { + final SpecMilestone milestone = SpecMilestone.forName(headers.get(HEADER_CONSENSUS_VERSION)); + return getSchema + .apply(schemaDefinitionCache.getSchemaDefinition(milestone)) + .getJsonTypeDefinition(); + } catch (Exception e) { + throw new BadRequestException( + String.format("(%s) header value was unexpected", HEADER_CONSENSUS_VERSION)); + } + } + private static Optional getSlot(final String json, final String... path) { try { return JsonUtil.getAttribute(json, CoreTypes.UINT64_TYPE, path); diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2.java index 77d3d22299d..4217eb91480 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2.java @@ -14,31 +14,34 @@ package tech.pegasys.teku.beaconrestapi.handlers.v2.beacon; import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.headerBasedSelector; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON; import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.function.Predicate; import tech.pegasys.teku.api.DataProvider; import tech.pegasys.teku.api.NodeDataProvider; -import tech.pegasys.teku.api.schema.Version; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +import tech.pegasys.teku.infrastructure.json.types.SerializableOneOfTypeDefinition; +import tech.pegasys.teku.infrastructure.json.types.SerializableOneOfTypeDefinitionBuilder; import tech.pegasys.teku.infrastructure.restapi.endpoints.AsyncApiResponse; import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata; import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint; import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiRequest; +import tech.pegasys.teku.infrastructure.restapi.openapi.request.OneOfJsonRequestContentTypeDefinition; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing; import tech.pegasys.teku.spec.schemas.SchemaDefinitionCache; +import tech.pegasys.teku.spec.schemas.SchemaDefinitions; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; import tech.pegasys.teku.statetransition.validation.ValidationResultCode; public class PostAttesterSlashingV2 extends RestApiEndpoint { public static final String ROUTE = "/eth/v2/beacon/pool/attester_slashings"; private final NodeDataProvider nodeDataProvider; - private final SchemaDefinitionCache schemaDefinitionCache; public PostAttesterSlashingV2( final DataProvider dataProvider, final SchemaDefinitionCache schemaDefinitionCache) { @@ -47,32 +50,64 @@ public PostAttesterSlashingV2( public PostAttesterSlashingV2( final NodeDataProvider provider, final SchemaDefinitionCache schemaDefinitionCache) { - super( - EndpointMetadata.post(ROUTE) - .operationId("submitPoolAttesterSlashingsV2") - .summary("Submit AttesterSlashing object to node's pool") - .description( - "Submits AttesterSlashing object to node's pool. Upon successful validation the node MUST broadcast it to network.") - .tags(TAG_BEACON) - .requestBodyType(getRequestType(schemaDefinitionCache)) - .headerRequired( - ETH_CONSENSUS_VERSION_TYPE.withDescription( - "Version of the attester slashing being submitted.")) - .response(SC_OK, "Success") - .build()); + super(createMetadata(schemaDefinitionCache)); this.nodeDataProvider = provider; - this.schemaDefinitionCache = schemaDefinitionCache; + } + + private static EndpointMetadata createMetadata( + final SchemaDefinitionCache schemaDefinitionCache) { + + final DeserializableTypeDefinition attesterSlashingPhase0Schema = + schemaDefinitionCache + .getSchemaDefinition(SpecMilestone.PHASE0) + .getAttesterSlashingSchema() + .getJsonTypeDefinition(); + + final DeserializableTypeDefinition attesterSlashingElectraSchema = + schemaDefinitionCache + .getSchemaDefinition(SpecMilestone.ELECTRA) + .getAttesterSlashingSchema() + .getJsonTypeDefinition(); + + final SerializableOneOfTypeDefinition attesterSlashingSchemaDefinition = + new SerializableOneOfTypeDefinitionBuilder() + .withType( + electraAttesterSlashingsPredicate(schemaDefinitionCache), + attesterSlashingElectraSchema) + .withType( + phase0AttesterSlashingsPredicate(schemaDefinitionCache), + attesterSlashingPhase0Schema) + .build(); + + final OneOfJsonRequestContentTypeDefinition.BodyTypeSelector + attesterSlashingBodySelector = + context -> + headerBasedSelector( + context.getHeaders(), + schemaDefinitionCache, + SchemaDefinitions::getAttesterSlashingSchema); + + return EndpointMetadata.post(ROUTE) + .operationId("submitPoolAttesterSlashingsV2") + .summary("Submit AttesterSlashing object to node's pool") + .description( + "Submits AttesterSlashing object to node's pool. Upon successful validation the node MUST broadcast it to network.") + .tags(TAG_BEACON) + .requestBodyType(attesterSlashingSchemaDefinition, attesterSlashingBodySelector) + .headerRequired( + ETH_CONSENSUS_VERSION_TYPE.withDescription( + "Version of the attester slashing being submitted.")) + .response(SC_OK, "Success") + .build(); } @Override public void handleRequest(final RestApiRequest request) throws JsonProcessingException { - handleAttesterSlashingRequest(request, nodeDataProvider, schemaDefinitionCache); + handleAttesterSlashingRequest(request, nodeDataProvider); } public static void handleAttesterSlashingRequest( - final RestApiRequest request, - final NodeDataProvider nodeDataProvider, - final SchemaDefinitionCache schemaDefinitionCache) + final RestApiRequest request, final NodeDataProvider nodeDataProvider) throws JsonProcessingException { final AttesterSlashing attesterSlashing = request.getRequestBody(); final SafeFuture future = @@ -90,30 +125,25 @@ public static void handleAttesterSlashingRequest( .orElse( "Invalid attester slashing, it will never pass validation so it's rejected.")); } else { - request.header( - HEADER_CONSENSUS_VERSION, - Version.fromMilestone( - getMilestoneFromAttesterSlashing( - attesterSlashing, schemaDefinitionCache)) - .name()); + return AsyncApiResponse.respondWithCode(SC_OK); } })); } - private static SpecMilestone getMilestoneFromAttesterSlashing( - final AttesterSlashing attesterSlashing, final SchemaDefinitionCache schemaDefinitionCache) { - return schemaDefinitionCache.milestoneAtSlot( - attesterSlashing.getAttestation1().getData().getSlot()); - } - - private static DeserializableTypeDefinition getRequestType( + private static Predicate electraAttesterSlashingsPredicate( final SchemaDefinitionCache schemaDefinitionCache) { - final AttesterSlashing.AttesterSlashingSchema attesterSlashingSchema = + return attesterSlashings -> schemaDefinitionCache - .getSchemaDefinition(SpecMilestone.ELECTRA) - .getAttesterSlashingSchema(); + .milestoneAtSlot(attesterSlashings.getAttestation1().getData().getSlot()) + .isGreaterThanOrEqualTo(SpecMilestone.ELECTRA); + } - return attesterSlashingSchema.getJsonTypeDefinition(); + private static Predicate phase0AttesterSlashingsPredicate( + final SchemaDefinitionCache schemaDefinitionCache) { + return attesterSlashings -> + !schemaDefinitionCache + .milestoneAtSlot(attesterSlashings.getAttestation1().getData().getSlot()) + .isGreaterThanOrEqualTo(SpecMilestone.ELECTRA); } } From 70b60520ed8def12a797a36e7297bf088eaaec42 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Wed, 31 Jul 2024 19:39:47 +1000 Subject: [PATCH 2/2] fix tests Signed-off-by: Gabriel Fukushima --- ...PostAttesterSlashingV2IntegrationTest.java | 89 ++++++++++++++++--- ...eth_v2_beacon_pool_attester_slashings.json | 7 +- .../v2/beacon/PostAttesterSlashingV2Test.java | 24 ++--- .../restapi/MetadataTestUtil.java | 10 +++ 4 files changed, 98 insertions(+), 32 deletions(-) diff --git a/data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v2/beacon/PostAttesterSlashingV2IntegrationTest.java b/data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v2/beacon/PostAttesterSlashingV2IntegrationTest.java index d695c9dbeda..8e2284a1ac2 100644 --- a/data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v2/beacon/PostAttesterSlashingV2IntegrationTest.java +++ b/data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v2/beacon/PostAttesterSlashingV2IntegrationTest.java @@ -21,16 +21,22 @@ import static tech.pegasys.teku.spec.SpecMilestone.ELECTRA; import static tech.pegasys.teku.spec.SpecMilestone.PHASE0; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.Collections; +import java.util.Locale; +import java.util.Optional; import okhttp3.Response; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestTemplate; import tech.pegasys.teku.api.schema.AttesterSlashing; import tech.pegasys.teku.beaconrestapi.AbstractDataBackedRestAPIIntegrationTest; import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostAttesterSlashingV2; import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.json.JsonTestUtil; +import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.TestSpecContext; -import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.TestSpecInvocationContextProvider; import tech.pegasys.teku.spec.util.DataStructureUtil; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; @@ -38,28 +44,35 @@ public class PostAttesterSlashingV2IntegrationTest extends AbstractDataBackedRestAPIIntegrationTest { - private final DataStructureUtil dataStructureUtil = - new DataStructureUtil(TestSpecFactory.createMinimalElectra()); + private DataStructureUtil dataStructureUtil; + private SpecMilestone specMilestone; @BeforeEach - public void setup() { - startRestAPIAtGenesis(); + void setup(final TestSpecInvocationContextProvider.SpecContext specContext) { + spec = specContext.getSpec(); + specMilestone = specContext.getSpecMilestone(); + startRestAPIAtGenesis(specMilestone); + dataStructureUtil = specContext.getDataStructureUtil(); } - @Test + @TestTemplate public void shouldReturnBadRequestWhenRequestBodyIsEmpty() throws Exception { final Response response = post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON("")); Assertions.assertThat(response.code()).isEqualTo(SC_BAD_REQUEST); } - @Test + @TestTemplate public void shouldReturnBadRequestWhenRequestBodyIsInvalid() throws Exception { final Response response = - post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON("{\"foo\": \"bar\"}")); + post( + PostAttesterSlashingV2.ROUTE, + jsonProvider.objectToJSON("{\"foo\": \"bar\"}"), + Collections.emptyMap(), + Optional.of(specMilestone.name().toLowerCase(Locale.ROOT))); assertThat(response.code()).isEqualTo(400); } - @Test + @TestTemplate public void shouldReturnServerErrorWhenUnexpectedErrorHappens() throws Exception { final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing = dataStructureUtil.randomAttesterSlashing(); @@ -69,11 +82,15 @@ public void shouldReturnServerErrorWhenUnexpectedErrorHappens() throws Exception doThrow(new RuntimeException()).when(attesterSlashingPool).addLocal(slashing); final Response response = - post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON(schemaSlashing)); + post( + PostAttesterSlashingV2.ROUTE, + jsonProvider.objectToJSON(schemaSlashing), + Collections.emptyMap(), + Optional.of(specMilestone.name().toLowerCase(Locale.ROOT))); assertThat(response.code()).isEqualTo(500); } - @Test + @TestTemplate public void shouldReturnSuccessWhenRequestBodyIsValid() throws Exception { final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing = dataStructureUtil.randomAttesterSlashing(); @@ -84,10 +101,56 @@ public void shouldReturnSuccessWhenRequestBodyIsValid() throws Exception { .thenReturn(SafeFuture.completedFuture(InternalValidationResult.ACCEPT)); final Response response = - post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON(schemaSlashing)); + post( + PostAttesterSlashingV2.ROUTE, + jsonProvider.objectToJSON(schemaSlashing), + Collections.emptyMap(), + Optional.of(specMilestone.name().toLowerCase(Locale.ROOT))); verify(attesterSlashingPool).addLocal(slashing); assertThat(response.code()).isEqualTo(200); } + + @TestTemplate + void shouldFailWhenMissingConsensusHeader() throws Exception { + final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing = + dataStructureUtil.randomAttesterSlashing(); + + final AttesterSlashing schemaSlashing = new AttesterSlashing(slashing); + + final Response response = + post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON(schemaSlashing)); + + assertThat(response.code()).isEqualTo(SC_BAD_REQUEST); + + final JsonNode resultAsJsonNode = JsonTestUtil.parseAsJsonNode(response.body().string()); + assertThat(resultAsJsonNode.get("message").asText()) + .isEqualTo("(Eth-Consensus-Version) header value was unexpected"); + } + + @TestTemplate + void shouldFailWhenBadConsensusHeaderValue() throws Exception { + + final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing = + dataStructureUtil.randomAttesterSlashing(); + + final AttesterSlashing schemaSlashing = new AttesterSlashing(slashing); + + when(attesterSlashingPool.addLocal(slashing)) + .thenReturn(SafeFuture.completedFuture(InternalValidationResult.ACCEPT)); + + final Response response = + post( + PostAttesterSlashingV2.ROUTE, + jsonProvider.objectToJSON(schemaSlashing), + Collections.emptyMap(), + Optional.of("NonExistingMileStone")); + + assertThat(response.code()).isEqualTo(SC_BAD_REQUEST); + + final JsonNode resultAsJsonNode = JsonTestUtil.parseAsJsonNode(response.body().string()); + assertThat(resultAsJsonNode.get("message").asText()) + .isEqualTo("(Eth-Consensus-Version) header value was unexpected"); + } } diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v2_beacon_pool_attester_slashings.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v2_beacon_pool_attester_slashings.json index a758809d246..f5800765c00 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v2_beacon_pool_attester_slashings.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v2_beacon_pool_attester_slashings.json @@ -56,7 +56,12 @@ "content" : { "application/json" : { "schema" : { - "$ref" : "#/components/schemas/AttesterSlashingElectra" + "type" : "object", + "oneOf" : [ { + "$ref" : "#/components/schemas/AttesterSlashingElectra" + }, { + "$ref" : "#/components/schemas/AttesterSlashingPhase0" + } ] } } } diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2Test.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2Test.java index da612177c97..026dc84ab40 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2Test.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v2/beacon/PostAttesterSlashingV2Test.java @@ -16,10 +16,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; -import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getRequestBodyFromMetadata; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataEmptyResponse; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse; @@ -29,9 +29,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.io.Resources; import java.io.IOException; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; -import tech.pegasys.teku.api.schema.Version; import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerTest; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.http.HttpErrorResponse; @@ -70,21 +70,6 @@ void shouldBeAbleToSubmitSlashing() throws Exception { assertThat(request.getResponseBody()).isNull(); } - @TestTemplate - void successfulResponseShouldContainVersionInTheHeader() throws Exception { - final AttesterSlashing slashing = dataStructureUtil.randomAttesterSlashing(); - request.setRequestBody(slashing); - - when(nodeDataProvider.postAttesterSlashing(slashing)) - .thenReturn(SafeFuture.completedFuture(InternalValidationResult.ACCEPT)); - - handler.handleRequest(request); - - assertThat(request.getResponseCode()).isEqualTo(SC_OK); - assertThat(request.getResponseHeaders(ETH_CONSENSUS_VERSION_TYPE.getName())) - .isEqualTo(Version.fromMilestone(specMilestone).name()); - } - @TestTemplate void shouldReturnBadRequestIfAttesterSlashingIsInvalid() throws Exception { final AttesterSlashing slashing = dataStructureUtil.randomAttesterSlashing(); @@ -107,7 +92,10 @@ void shouldReadRequestBody() throws IOException { Resources.getResource( PostAttesterSlashingV2Test.class, "postAttesterSlashingRequestBody.json"), UTF_8); - assertThat(getRequestBodyFromMetadata(handler, data)).isInstanceOf(AttesterSlashing.class); + assertThat( + getRequestBodyFromMetadata( + handler, Map.of(HEADER_CONSENSUS_VERSION, specMilestone.name()), data)) + .isInstanceOf(AttesterSlashing.class); } @TestTemplate diff --git a/infrastructure/restapi/src/testFixtures/java/tech/pegasys/teku/infrastructure/restapi/MetadataTestUtil.java b/infrastructure/restapi/src/testFixtures/java/tech/pegasys/teku/infrastructure/restapi/MetadataTestUtil.java index 58897af77c9..12713572162 100644 --- a/infrastructure/restapi/src/testFixtures/java/tech/pegasys/teku/infrastructure/restapi/MetadataTestUtil.java +++ b/infrastructure/restapi/src/testFixtures/java/tech/pegasys/teku/infrastructure/restapi/MetadataTestUtil.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.Map; import org.apache.commons.io.IOUtils; import org.assertj.core.api.AssertionsForClassTypes; import tech.pegasys.teku.infrastructure.http.ContentTypes; @@ -72,6 +73,15 @@ public static Object getRequestBodyFromMetadata(final RestApiEndpoint handler, f IOUtils.toInputStream(json, StandardCharsets.UTF_8)); } + public static Object getRequestBodyFromMetadata( + final RestApiEndpoint handler, final Map headers, final String json) + throws IOException { + final RequestContentTypeDefinition requestContentTypeDefinition = + handler.getMetadata().getRequestBodyType(); + return requestContentTypeDefinition.deserialize( + IOUtils.toInputStream(json, StandardCharsets.UTF_8), headers); + } + private static byte[] toBytes(final Serializer func) throws JsonProcessingException { final ByteArrayOutputStream out = new ByteArrayOutputStream(); func.serialize(out);