diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/LengthValidatingInputStream.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/LengthValidatingInputStream.java index 4e7a80df03074..93ed7ae6cbd3d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/LengthValidatingInputStream.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/LengthValidatingInputStream.java @@ -8,13 +8,10 @@ import java.io.InputStream; import java.util.Objects; -import static io.clientcore.core.implementation.http.rest.RestProxyUtils.bodyTooLarge; -import static io.clientcore.core.implementation.http.rest.RestProxyUtils.bodyTooSmall; - /** * An {@link InputStream} decorator that tracks the number of bytes read from an inner {@link InputStream} and throws * an exception if the number of bytes read doesn't match what was expected. - * + *

* This implementation assumes that reader is going to read until EOF. */ final class LengthValidatingInputStream extends InputStream { @@ -112,9 +109,9 @@ private void validateLength(int readSize) { if (readSize == -1) { // If the inner InputStream has reached termination validate that the read bytes matches what was expected. if (position > expectedReadSize) { - throw new IllegalStateException(bodyTooLarge(position, expectedReadSize)); + throw new IllegalStateException(RestProxyImpl.bodyTooLarge(position, expectedReadSize)); } else if (position < expectedReadSize) { - throw new IllegalStateException(bodyTooSmall(position, expectedReadSize)); + throw new IllegalStateException(RestProxyImpl.bodyTooSmall(position, expectedReadSize)); } } else { position += readSize; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyBase.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyBase.java deleted file mode 100644 index 4c4a3094bfd63..0000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyBase.java +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.implementation.http.rest; - -import io.clientcore.core.http.exception.HttpExceptionType; -import io.clientcore.core.http.exception.HttpResponseException; -import io.clientcore.core.http.models.ContentType; -import io.clientcore.core.http.models.HttpHeaderName; -import io.clientcore.core.http.models.HttpHeaders; -import io.clientcore.core.http.models.HttpRequest; -import io.clientcore.core.http.models.RequestOptions; -import io.clientcore.core.http.models.Response; -import io.clientcore.core.http.pipeline.HttpPipeline; -import io.clientcore.core.implementation.ReflectiveInvoker; -import io.clientcore.core.implementation.TypeUtil; -import io.clientcore.core.implementation.http.UnexpectedExceptionInformation; -import io.clientcore.core.implementation.http.serializer.CompositeSerializer; -import io.clientcore.core.implementation.http.serializer.MalformedValueException; -import io.clientcore.core.implementation.util.UriBuilder; -import io.clientcore.core.util.ClientLogger; -import io.clientcore.core.util.binarydata.BinaryData; -import io.clientcore.core.util.serializer.ObjectSerializer; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.reflect.Type; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -public abstract class RestProxyBase { - static final ResponseConstructorsCache RESPONSE_CONSTRUCTORS_CACHE = new ResponseConstructorsCache(); - - // RestProxy is a commonly used class, use a static logger. - static final ClientLogger LOGGER = new ClientLogger(RestProxyBase.class); - - final HttpPipeline httpPipeline; - final CompositeSerializer serializer; - final SwaggerInterfaceParser interfaceParser; - - /** - * Create a RestProxy. - * - * @param httpPipeline The HttpPipelinePolicy and HttpClient httpPipeline that will be used to send HTTP requests. - * @param interfaceParser The parser that contains information about the interface describing REST API methods that - * this RestProxy "implements". - * @param serializers The serializers that will be used to convert response bodies to POJOs. - */ - public RestProxyBase(HttpPipeline httpPipeline, SwaggerInterfaceParser interfaceParser, - ObjectSerializer... serializers) { - this.httpPipeline = httpPipeline; - this.interfaceParser = interfaceParser; - this.serializer = new CompositeSerializer(Arrays.asList(serializers)); - } - - public final Object invoke(Object proxy, RequestOptions options, SwaggerMethodParser methodParser, Object[] args) { - try { - HttpRequest request = createHttpRequest(methodParser, serializer, args).setRequestOptions(options) - .setServerSentEventListener(methodParser.setServerSentEventListener(args)); - - return invoke(proxy, methodParser, request); - } catch (IOException e) { - throw LOGGER.logThrowableAsError(new UncheckedIOException(e)); - } catch (URISyntaxException e) { - throw LOGGER.logThrowableAsError(new RuntimeException(e)); - } - } - - protected abstract Object invoke(Object proxy, SwaggerMethodParser methodParser, HttpRequest request); - - public abstract void updateRequest(RequestDataConfiguration requestDataConfiguration, - CompositeSerializer serializer) throws IOException; - - @SuppressWarnings({ "unchecked" }) - public Response createResponseIfNecessary(Response response, Type entityType, Object bodyAsObject) { - final Class> clazz = (Class>) TypeUtil.getRawClass(entityType); - - // Inspection of the response type needs to be performed to determine the course of action: either return the - // Response or rely on reflection to create an appropriate Response subtype. - if (clazz.equals(Response.class)) { - // Return the Response. - return response; - } else { - // Otherwise, rely on reflection, for now, to get the best constructor to use to create the Response - // subtype. - // - // Ideally, in the future the SDKs won't need to dabble in reflection here as the Response subtypes should - // be given a way to register their constructor as a callback method that consumes Response and the body as - // an Object. - ReflectiveInvoker constructorReflectiveInvoker = RESPONSE_CONSTRUCTORS_CACHE.get(clazz); - - return RESPONSE_CONSTRUCTORS_CACHE.invoke(constructorReflectiveInvoker, response, bodyAsObject); - } - } - - /** - * Create an HttpRequest for the provided Swagger method using the provided arguments. - * - * @param methodParser The Swagger method parser to use. - * @param args The arguments to use to populate the method's annotation values. - * @return An HttpRequest. - * @throws IOException If the body contents cannot be serialized. - */ - HttpRequest createHttpRequest(SwaggerMethodParser methodParser, CompositeSerializer serializer, Object[] args) - throws IOException, URISyntaxException { - - // Sometimes people pass in a full URI for the value of their PathParam annotated argument. - // This definitely happens in paging scenarios. In that case, just use the full URI and - // ignore the Host annotation. - final String path = methodParser.setPath(args, serializer); - final UriBuilder pathUriBuilder = UriBuilder.parse(path); - final UriBuilder uriBuilder; - - if (pathUriBuilder.getScheme() != null) { - uriBuilder = pathUriBuilder; - } else { - uriBuilder = new UriBuilder(); - - methodParser.setSchemeAndHost(args, uriBuilder, serializer); - - // Set the path after host, concatenating the path segment in the host. - if (path != null && !path.isEmpty() && !"/".equals(path)) { - String hostPath = uriBuilder.getPath(); - - if (hostPath == null || hostPath.isEmpty() || "/".equals(hostPath) || path.contains("://")) { - uriBuilder.setPath(path); - } else { - if (path.startsWith("/")) { - uriBuilder.setPath(hostPath + path); - } else { - uriBuilder.setPath(hostPath + "/" + path); - } - } - } - } - - methodParser.setEncodedQueryParameters(args, uriBuilder, serializer); - - final URI uri = uriBuilder.toUri(); - final HttpRequest request - = configRequest(new HttpRequest(methodParser.getHttpMethod(), uri), methodParser, serializer, args); - // Headers from Swagger method arguments always take precedence over inferred headers from body types - HttpHeaders httpHeaders = request.getHeaders(); - - methodParser.setHeaders(args, httpHeaders, serializer); - - return request; - } - - private HttpRequest configRequest(HttpRequest request, SwaggerMethodParser methodParser, - CompositeSerializer objectSerializer, Object[] args) throws IOException { - final Object bodyContentObject = methodParser.setBody(args, serializer); - - if (bodyContentObject == null) { - request.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, "0"); - } else { - // We read the content type from the @BodyParam annotation - String contentType = methodParser.getBodyContentType(); - - // If this is null or empty, the service interface definition is incomplete and should - // be fixed to ensure correct definitions are applied - if (contentType == null || contentType.isEmpty()) { - if (bodyContentObject instanceof byte[] || bodyContentObject instanceof String) { - contentType = ContentType.APPLICATION_OCTET_STREAM; - } else { - contentType = ContentType.APPLICATION_JSON; - } - } - - request.getHeaders().set(HttpHeaderName.CONTENT_TYPE, contentType); - - if (bodyContentObject instanceof BinaryData) { - BinaryData binaryData = (BinaryData) bodyContentObject; - - if (binaryData.getLength() != null) { - request.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, binaryData.getLength().toString()); - } - - // The request body is not read here. BinaryData lazily converts the underlying content which is then - // read by HttpClient implementations when sending the request to the service. There is no memory - // copy that happens here. Sources like InputStream or File will not be eagerly copied into memory - // until it's required by the HttpClient implementations. - request.setBody(binaryData); - - return request; - } - - // TODO(jogiles) this feels hacky - boolean isJson = false; - final String[] contentTypeParts = contentType.split(";"); - - for (final String contentTypePart : contentTypeParts) { - if (contentTypePart.trim().equalsIgnoreCase(ContentType.APPLICATION_JSON)) { - isJson = true; - - break; - } - } - - updateRequest(new RequestDataConfiguration(request, methodParser, isJson, bodyContentObject), - objectSerializer); - } - - return request; - } - - /** - * Creates an HttpResponseException exception using the details provided in http response and its content. - * - * @param unexpectedExceptionInformation The exception holding UnexpectedException's details. - * @param response The http response to parse when constructing exception - * @param responseBody The response body to use when constructing exception - * @param responseDecodedBody The decoded response content to use when constructing exception - * @return The {@link HttpResponseException} created from the provided details. - */ - public static HttpResponseException instantiateUnexpectedException( - UnexpectedExceptionInformation unexpectedExceptionInformation, Response response, BinaryData responseBody, - Object responseDecodedBody) { - StringBuilder exceptionMessage - = new StringBuilder("Status code ").append(response.getStatusCode()).append(", "); - - final String contentType = response.getHeaders().getValue(HttpHeaderName.CONTENT_TYPE); - - if ("application/octet-stream".equalsIgnoreCase(contentType)) { - String contentLength = response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH); - - exceptionMessage.append("(").append(contentLength).append("-byte body)"); - } else if (responseBody == null || responseBody.toBytes().length == 0) { - exceptionMessage.append("(empty body)"); - } else { - exceptionMessage.append('\"') - .append(new String(responseBody.toBytes(), StandardCharsets.UTF_8)) - .append('\"'); - } - - // If the decoded response body is on of these exception types there was a failure in creating the actual - // exception body type. In this case return an HttpResponseException to maintain the exception having a - // reference to the Response and information about what caused the deserialization failure. - if (responseDecodedBody instanceof IOException - || responseDecodedBody instanceof MalformedValueException - || responseDecodedBody instanceof IllegalStateException) { - - return new HttpResponseException(exceptionMessage.toString(), response, null, - (Throwable) responseDecodedBody); - } - - HttpExceptionType exceptionType = unexpectedExceptionInformation.getExceptionType(); - - return new HttpResponseException(exceptionMessage.toString(), response, exceptionType, responseDecodedBody); - } -} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyImpl.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyImpl.java index 20647c2b5514a..3759bb3c2d9c8 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyImpl.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyImpl.java @@ -3,6 +3,9 @@ package io.clientcore.core.implementation.http.rest; +import io.clientcore.core.http.exception.HttpExceptionType; +import io.clientcore.core.http.exception.HttpResponseException; +import io.clientcore.core.http.models.ContentType; import io.clientcore.core.http.models.HttpHeaderName; import io.clientcore.core.http.models.HttpHeaders; import io.clientcore.core.http.models.HttpMethod; @@ -13,12 +16,18 @@ import io.clientcore.core.http.models.ResponseBodyMode; import io.clientcore.core.http.pipeline.HttpPipeline; import io.clientcore.core.implementation.ReflectionSerializable; +import io.clientcore.core.implementation.ReflectiveInvoker; import io.clientcore.core.implementation.TypeUtil; import io.clientcore.core.implementation.http.HttpResponseAccessHelper; +import io.clientcore.core.implementation.http.UnexpectedExceptionInformation; import io.clientcore.core.implementation.http.serializer.CompositeSerializer; +import io.clientcore.core.implementation.http.serializer.MalformedValueException; import io.clientcore.core.implementation.util.Base64Uri; import io.clientcore.core.implementation.util.ImplUtils; +import io.clientcore.core.implementation.util.UriBuilder; +import io.clientcore.core.util.ClientLogger; import io.clientcore.core.util.binarydata.BinaryData; +import io.clientcore.core.util.binarydata.InputStreamBinaryData; import io.clientcore.core.util.serializer.ObjectSerializer; import io.clientcore.core.util.serializer.SerializationFormat; @@ -26,12 +35,24 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import static io.clientcore.core.http.models.ResponseBodyMode.DESERIALIZE; import static io.clientcore.core.implementation.http.serializer.HttpResponseBodyDecoder.decodeByteArray; -public class RestProxyImpl extends RestProxyBase { +public class RestProxyImpl { + static final ResponseConstructorsCache RESPONSE_CONSTRUCTORS_CACHE = new ResponseConstructorsCache(); + + // RestProxy is a commonly used class, use a static logger. + static final ClientLogger LOGGER = new ClientLogger(RestProxyImpl.class); + + final HttpPipeline httpPipeline; + final CompositeSerializer serializer; + final SwaggerInterfaceParser interfaceParser; /** * Create a RestProxy. @@ -43,36 +64,210 @@ public class RestProxyImpl extends RestProxyBase { */ public RestProxyImpl(HttpPipeline httpPipeline, SwaggerInterfaceParser interfaceParser, ObjectSerializer... serializers) { - super(httpPipeline, interfaceParser, serializers); + this.httpPipeline = httpPipeline; + this.interfaceParser = interfaceParser; + this.serializer = new CompositeSerializer(Arrays.asList(serializers)); } /** - * Send the provided request, applying any request policies provided to the HttpClient instance. + * Invokes the provided method using the provided arguments. * - * @param request the HTTP request to send. + * @param proxy The proxy object to invoke the method on. + * @param options The RequestOptions to use for the request. + * @param methodParser The SwaggerMethodParser that contains information about the method to invoke. + * @param args The arguments to use when invoking the method. + * @return The result of invoking the method. + * @throws UncheckedIOException When an I/O error occurs. + * @throws RuntimeException When a URI syntax error occurs. + */ + @SuppressWarnings({ "try", "unused" }) + public final Object invoke(Object proxy, RequestOptions options, SwaggerMethodParser methodParser, Object[] args) { + try { + HttpRequest request = createHttpRequest(methodParser, serializer, args).setRequestOptions(options) + .setServerSentEventListener(methodParser.setServerSentEventListener(args)); + + // If there is 'RequestOptions' apply its request callback operations before validating the body. + // This is because the callbacks may mutate the request body. + if (request.getRequestOptions() != null) { + request.getRequestOptions().getRequestCallback().accept(request); + } + + if (request.getBody() != null) { + request.setBody(RestProxyImpl.validateLength(request)); + } + + final Response response = httpPipeline.send(request); + + return handleRestReturnType(response, methodParser, methodParser.getReturnType(), serializer); + } catch (IOException e) { + throw LOGGER.logThrowableAsError(new UncheckedIOException(e)); + } catch (URISyntaxException e) { + throw LOGGER.logThrowableAsError(new RuntimeException(e)); + } + } + + /** + * Validates the Length of the input request matches its configured Content Length. * - * @return A {@link Response}. + * @param request the input request to validate. + * @return the requests body as BinaryData on successful validation. */ - Response send(HttpRequest request) { - return httpPipeline.send(request); + static BinaryData validateLength(final HttpRequest request) { + final BinaryData binaryData = request.getBody(); + + if (binaryData == null) { + return null; + } + + final long expectedLength = Long.parseLong(request.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH)); + + if (binaryData instanceof InputStreamBinaryData) { + InputStream inputStream = binaryData.toStream(); + LengthValidatingInputStream lengthValidatingInputStream + = new LengthValidatingInputStream(inputStream, expectedLength); + + return BinaryData.fromStream(lengthValidatingInputStream, expectedLength); + } else { + if (binaryData.getLength() == null) { + byte[] b = binaryData.toBytes(); + + validateLengthInternal(b.length, expectedLength); + + return BinaryData.fromBytes(b); + } else { + validateLengthInternal(binaryData.getLength(), expectedLength); + + return binaryData; + } + } } - @SuppressWarnings({ "try", "unused" }) - @Override - public Object invoke(Object proxy, SwaggerMethodParser methodParser, HttpRequest request) { - // If there is 'RequestOptions' apply its request callback operations before validating the body. - // This is because the callbacks may mutate the request body. - if (request.getRequestOptions() != null) { - request.getRequestOptions().getRequestCallback().accept(request); + private static void validateLengthInternal(long length, long expectedLength) { + if (length > expectedLength) { + throw new IllegalStateException(bodyTooLarge(length, expectedLength)); } - if (request.getBody() != null) { - request.setBody(RestProxyUtils.validateLength(request)); + if (length < expectedLength) { + throw new IllegalStateException(bodyTooSmall(length, expectedLength)); } + } + + static String bodyTooLarge(long length, long expectedLength) { + return "Request body emitted " + length + " bytes, more than the expected " + expectedLength + " bytes."; + } + + static String bodyTooSmall(long length, long expectedLength) { + return "Request body emitted " + length + " bytes, less than the expected " + expectedLength + " bytes."; + } + + /** + * Create an HttpRequest for the provided Swagger method using the provided arguments. + * + * @param methodParser The Swagger method parser to use. + * @param args The arguments to use to populate the method's annotation values. + * @return An HttpRequest. + * @throws IOException If the body contents cannot be serialized. + */ + private static HttpRequest createHttpRequest(SwaggerMethodParser methodParser, CompositeSerializer serializer, + Object[] args) throws IOException, URISyntaxException { + + // Sometimes people pass in a full URI for the value of their PathParam annotated argument. + // This definitely happens in paging scenarios. In that case, just use the full URI and + // ignore the Host annotation. + final String path = methodParser.setPath(args, serializer); + final UriBuilder pathUriBuilder = UriBuilder.parse(path); + final UriBuilder uriBuilder; + + if (pathUriBuilder.getScheme() != null) { + uriBuilder = pathUriBuilder; + } else { + uriBuilder = new UriBuilder(); + + methodParser.setSchemeAndHost(args, uriBuilder, serializer); + + // Set the path after host, concatenating the path segment in the host. + if (path != null && !path.isEmpty() && !"/".equals(path)) { + String hostPath = uriBuilder.getPath(); + + if (hostPath == null || hostPath.isEmpty() || "/".equals(hostPath) || path.contains("://")) { + uriBuilder.setPath(path); + } else { + if (path.startsWith("/")) { + uriBuilder.setPath(hostPath + path); + } else { + uriBuilder.setPath(hostPath + "/" + path); + } + } + } + } + + methodParser.setEncodedQueryParameters(args, uriBuilder, serializer); + + final URI uri = uriBuilder.toUri(); + final HttpRequest request + = configRequest(new HttpRequest(methodParser.getHttpMethod(), uri), methodParser, serializer, args); + // Headers from Swagger method arguments always take precedence over inferred headers from body types + HttpHeaders httpHeaders = request.getHeaders(); + + methodParser.setHeaders(args, httpHeaders, serializer); - final Response response = send(request); + return request; + } + + private static HttpRequest configRequest(HttpRequest request, SwaggerMethodParser methodParser, + CompositeSerializer serializer, Object[] args) throws IOException { + final Object bodyContentObject = methodParser.setBody(args, serializer); + + if (bodyContentObject == null) { + request.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, "0"); + } else { + // We read the content type from the @BodyParam annotation + String contentType = methodParser.getBodyContentType(); + + // If this is null or empty, the service interface definition is incomplete and should + // be fixed to ensure correct definitions are applied + if (contentType == null || contentType.isEmpty()) { + if (bodyContentObject instanceof byte[] || bodyContentObject instanceof String) { + contentType = ContentType.APPLICATION_OCTET_STREAM; + } else { + contentType = ContentType.APPLICATION_JSON; + } + } + + request.getHeaders().set(HttpHeaderName.CONTENT_TYPE, contentType); + + if (bodyContentObject instanceof BinaryData) { + BinaryData binaryData = (BinaryData) bodyContentObject; + + if (binaryData.getLength() != null) { + request.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, binaryData.getLength().toString()); + } - return handleRestReturnType(response, methodParser, methodParser.getReturnType()); + // The request body is not read here. BinaryData lazily converts the underlying content which is then + // read by HttpClient implementations when sending the request to the service. There is no memory + // copy that happens here. Sources like InputStream or File will not be eagerly copied into memory + // until it's required by the HttpClient implementations. + request.setBody(binaryData); + + return request; + } + + // TODO(jogiles) this feels hacky + boolean isJson = false; + final String[] contentTypeParts = contentType.split(";"); + + for (final String contentTypePart : contentTypeParts) { + if (contentTypePart.trim().equalsIgnoreCase(ContentType.APPLICATION_JSON)) { + isJson = true; + + break; + } + } + + updateRequest(new RequestDataConfiguration(request, methodParser, isJson, bodyContentObject), serializer); + } + + return request; } /** @@ -85,10 +280,10 @@ public Object invoke(Object proxy, SwaggerMethodParser methodParser, HttpRequest * @param response The Response to check. * @param methodParser The method parser that contains information about the service interface method that initiated * the HTTP request. - * * @return The decodedResponse. */ - private Response ensureExpectedStatus(Response response, SwaggerMethodParser methodParser) { + private static Response ensureExpectedStatus(Response response, SwaggerMethodParser methodParser, + CompositeSerializer serializer) { int responseStatusCode = response.getStatusCode(); // If the response was success or configured to not return an error status when the request fails, return it. @@ -108,8 +303,53 @@ private Response ensureExpectedStatus(Response response, SwaggerMethodPars } } - private Object handleRestResponseReturnType(Response response, SwaggerMethodParser methodParser, - Type entityType) { + /** + * Creates an HttpResponseException exception using the details provided in http response and its content. + * + * @param unexpectedExceptionInformation The exception holding UnexpectedException's details. + * @param response The http response to parse when constructing exception + * @param responseBody The response body to use when constructing exception + * @param responseDecodedBody The decoded response content to use when constructing exception + * @return The {@link HttpResponseException} created from the provided details. + */ + private static HttpResponseException instantiateUnexpectedException( + UnexpectedExceptionInformation unexpectedExceptionInformation, Response response, BinaryData responseBody, + Object responseDecodedBody) { + StringBuilder exceptionMessage + = new StringBuilder("Status code ").append(response.getStatusCode()).append(", "); + + final String contentType = response.getHeaders().getValue(HttpHeaderName.CONTENT_TYPE); + + if ("application/octet-stream".equalsIgnoreCase(contentType)) { + String contentLength = response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH); + + exceptionMessage.append("(").append(contentLength).append("-byte body)"); + } else if (responseBody == null || responseBody.toBytes().length == 0) { + exceptionMessage.append("(empty body)"); + } else { + exceptionMessage.append('\"') + .append(new String(responseBody.toBytes(), StandardCharsets.UTF_8)) + .append('\"'); + } + + // If the decoded response body is on of these exception types there was a failure in creating the actual + // exception body type. In this case return an HttpResponseException to maintain the exception having a + // reference to the Response and information about what caused the deserialization failure. + if (responseDecodedBody instanceof IOException + || responseDecodedBody instanceof MalformedValueException + || responseDecodedBody instanceof IllegalStateException) { + + return new HttpResponseException(exceptionMessage.toString(), response, null, + (Throwable) responseDecodedBody); + } + + HttpExceptionType exceptionType = unexpectedExceptionInformation.getExceptionType(); + + return new HttpResponseException(exceptionMessage.toString(), response, exceptionType, responseDecodedBody); + } + + private static Object handleRestResponseReturnType(Response response, SwaggerMethodParser methodParser, + Type entityType, CompositeSerializer serializer) { if (TypeUtil.isTypeOrSubTypeOf(entityType, Response.class)) { final Type bodyType = TypeUtil.getRestResponseBodyType(entityType); @@ -131,10 +371,10 @@ private Object handleRestResponseReturnType(Response response, SwaggerMethodP if (responseBodyMode == DESERIALIZE) { HttpResponseAccessHelper.setValue((HttpResponse) response, - handleResponseBody(response, methodParser, bodyType, response.getBody())); + handleResponseBody(response, methodParser, bodyType, response.getBody(), serializer)); } else { HttpResponseAccessHelper.setBodyDeserializer((HttpResponse) response, - (body) -> handleResponseBody(response, methodParser, bodyType, body)); + (body) -> handleResponseBody(response, methodParser, bodyType, body, serializer)); } Response responseToReturn = createResponseIfNecessary(response, entityType, response.getBody()); @@ -148,12 +388,34 @@ private Object handleRestResponseReturnType(Response response, SwaggerMethodP } else { // When not handling a Response subtype, we need to eagerly read the response body to construct the correct // return type. - return handleResponseBody(response, methodParser, entityType, response.getBody()); + return handleResponseBody(response, methodParser, entityType, response.getBody(), serializer); } } - private Object handleResponseBody(Response response, SwaggerMethodParser methodParser, Type entityType, - BinaryData responseBody) { + @SuppressWarnings({ "unchecked" }) + private static Response createResponseIfNecessary(Response response, Type entityType, Object bodyAsObject) { + final Class> clazz = (Class>) TypeUtil.getRawClass(entityType); + + // Inspection of the response type needs to be performed to determine the course of action: either return the + // Response or rely on reflection to create an appropriate Response subtype. + if (clazz.equals(Response.class)) { + // Return the Response. + return response; + } else { + // Otherwise, rely on reflection, for now, to get the best constructor to use to create the Response + // subtype. + // + // Ideally, in the future the SDKs won't need to dabble in reflection here as the Response subtypes should + // be given a way to register their constructor as a callback method that consumes Response and the body as + // an Object. + ReflectiveInvoker constructorReflectiveInvoker = RESPONSE_CONSTRUCTORS_CACHE.get(clazz); + + return RESPONSE_CONSTRUCTORS_CACHE.invoke(constructorReflectiveInvoker, response, bodyAsObject); + } + } + + private static Object handleResponseBody(Response response, SwaggerMethodParser methodParser, Type entityType, + BinaryData responseBody, CompositeSerializer serializer) { final int responseStatusCode = response.getStatusCode(); final HttpMethod httpMethod = methodParser.getHttpMethod(); final Type returnValueWireType = methodParser.getReturnValueWireType(); @@ -195,11 +457,11 @@ private Object handleResponseBody(Response response, SwaggerMethodParser meth * @param response The HTTP response to the original HTTP request. * @param methodParser The SwaggerMethodParser that the request originates from. * @param returnType The type of value that will be returned. - * * @return The deserialized result. */ - private Object handleRestReturnType(Response response, SwaggerMethodParser methodParser, Type returnType) { - final Response expectedResponse = ensureExpectedStatus(response, methodParser); + private static Object handleRestReturnType(Response response, SwaggerMethodParser methodParser, Type returnType, + CompositeSerializer serializer) { + final Response expectedResponse = ensureExpectedStatus(response, methodParser, serializer); final Object result; if (TypeUtil.isTypeOrSubTypeOf(returnType, void.class) || TypeUtil.isTypeOrSubTypeOf(returnType, Void.class)) { @@ -211,13 +473,14 @@ private Object handleRestReturnType(Response response, SwaggerMethodParser me result = null; } else { - result = handleRestResponseReturnType(response, methodParser, returnType); + result = handleRestResponseReturnType(response, methodParser, returnType, serializer); } return result; } - public void updateRequest(RequestDataConfiguration requestDataConfiguration, CompositeSerializer serializer) { + private static void updateRequest(RequestDataConfiguration requestDataConfiguration, + CompositeSerializer serializer) { boolean isJson = requestDataConfiguration.isJson(); HttpRequest request = requestDataConfiguration.getHttpRequest(); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyUtils.java deleted file mode 100644 index 5fd08552d3ed9..0000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/http/rest/RestProxyUtils.java +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.implementation.http.rest; - -import io.clientcore.core.http.models.HttpHeaderName; -import io.clientcore.core.http.models.HttpRequest; -import io.clientcore.core.util.ClientLogger; -import io.clientcore.core.util.binarydata.BinaryData; -import io.clientcore.core.util.binarydata.InputStreamBinaryData; - -import java.io.InputStream; - -/** - * Utility methods that aid processing in RestProxy. - */ -public final class RestProxyUtils { - public static final ClientLogger LOGGER = new ClientLogger(RestProxyUtils.class); - - private RestProxyUtils() { - } - - /** - * Validates the Length of the input request matches its configured Content Length. - * - * @param request the input request to validate. - * - * @return the requests body as BinaryData on successful validation. - */ - public static BinaryData validateLength(final HttpRequest request) { - final BinaryData binaryData = request.getBody(); - - if (binaryData == null) { - return null; - } - - final long expectedLength = Long.parseLong(request.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH)); - - if (binaryData instanceof InputStreamBinaryData) { - InputStream inputStream = binaryData.toStream(); - LengthValidatingInputStream lengthValidatingInputStream - = new LengthValidatingInputStream(inputStream, expectedLength); - - return BinaryData.fromStream(lengthValidatingInputStream, expectedLength); - } else { - if (binaryData.getLength() == null) { - byte[] b = binaryData.toBytes(); - - validateLengthInternal(b.length, expectedLength); - - return BinaryData.fromBytes(b); - } else { - validateLengthInternal(binaryData.getLength(), expectedLength); - - return binaryData; - } - } - } - - private static void validateLengthInternal(long length, long expectedLength) { - if (length > expectedLength) { - throw new IllegalStateException(bodyTooLarge(length, expectedLength)); - } - - if (length < expectedLength) { - throw new IllegalStateException(bodyTooSmall(length, expectedLength)); - } - } - - static String bodyTooLarge(long length, long expectedLength) { - return "Request body emitted " + length + " bytes, more than the expected " + expectedLength + " bytes."; - } - - static String bodyTooSmall(long length, long expectedLength) { - return "Request body emitted " + length + " bytes, less than the expected " + expectedLength + " bytes."; - } -} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/RestProxyImplTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/RestProxyImplTests.java index 420bb87f7d596..692d6d36f80ff 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/RestProxyImplTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/RestProxyImplTests.java @@ -16,21 +16,37 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.http.pipeline.HttpPipeline; import io.clientcore.core.http.pipeline.HttpPipelineBuilder; +import io.clientcore.core.implementation.util.JsonSerializer; import io.clientcore.core.util.Context; import io.clientcore.core.util.binarydata.BinaryData; -import io.clientcore.core.implementation.util.JsonSerializer; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; +import static io.clientcore.core.util.TestUtils.assertArraysEqual; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests {@link RestProxy}. */ public class RestProxyImplTests { + private static final String SAMPLE = "sample"; + private static final byte[] EXPECTED = SAMPLE.getBytes(StandardCharsets.UTF_8); + @ServiceInterface(name = "myService", host = "https://somecloud.com") interface TestInterface { @HttpRequestInformation(method = HttpMethod.POST, path = "my/uri/path", expectedStatusCodes = { 200 }) @@ -89,4 +105,125 @@ public void close() throws IOException { }; } } + + @ParameterizedTest + @MethodSource("expectedBodyLengthDataProvider") + public void expectedBodyLength(HttpRequest httpRequest) { + BinaryData binaryData = RestProxyImpl.validateLength(httpRequest); + + assertNotNull(binaryData); + assertArraysEqual(EXPECTED, binaryData.toBytes()); + } + + public static Stream expectedBodyLengthDataProvider() throws Exception { + return dataProvider(EXPECTED.length); + } + + @Test + public void emptyRequestBody() { + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost"); + + assertNull(RestProxyImpl.validateLength(httpRequest)); + } + + @ParameterizedTest + @MethodSource("unexpectedBodyLengthTooSmallDataProvider") + public void unexpectedBodyLengthTooSmall(HttpRequest httpRequest) { + assertThrows(IllegalStateException.class, () -> validateAndCollectRequest(httpRequest), "Request body " + + "emitted " + EXPECTED.length + " bytes, less than the expected " + (EXPECTED.length + 1) + " bytes."); + } + + public static Stream unexpectedBodyLengthTooSmallDataProvider() throws Exception { + return dataProvider(EXPECTED.length + 1); + } + + @ParameterizedTest + @MethodSource("unexpectedBodyLengthTooLargeDataProvider") + public void unexpectedBodyLengthTooLarge(HttpRequest httpRequest) { + assertThrows(IllegalStateException.class, () -> validateAndCollectRequest(httpRequest), "Request body " + + "emitted " + EXPECTED.length + " bytes, more than the expected " + (EXPECTED.length - 1) + " bytes."); + } + + public static Stream unexpectedBodyLengthTooLargeDataProvider() throws Exception { + return dataProvider(EXPECTED.length - 1); + } + + @Test + public void multipleToBytesToCheckBodyLength() { + HttpRequest httpRequest + = new HttpRequest(HttpMethod.GET, "http://localhost").setBody(BinaryData.fromBytes(EXPECTED)); + httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length)); + + BinaryData binaryData = RestProxyImpl.validateLength(httpRequest); + + assertNotNull(binaryData); + assertArraysEqual(EXPECTED, binaryData.toBytes()); + assertArraysEqual(EXPECTED, binaryData.toBytes()); + } + + private static Stream dataProvider(int contentLength) throws Exception { + Path file = Files.createTempFile(RestProxyImpl.class.getSimpleName(), null); + file.toFile().deleteOnExit(); + Files.write(file, EXPECTED); + + return Stream.of( + Arguments.of(Named.of("bytes", + createHttpRequest("http://localhost", BinaryData.fromBytes(EXPECTED), contentLength))), + Arguments.of(Named.of("string", + createHttpRequest("http://localhost", BinaryData.fromString(SAMPLE), contentLength))), + Arguments.of( + Named.of("stream", createHttpRequest("http://localhost", BinaryData.fromFile(file), contentLength))), + Arguments + .of(Named.of("file", createHttpRequest("http://localhost", BinaryData.fromFile(file), contentLength)))); + } + + private static HttpRequest createHttpRequest(String uri, BinaryData body, int contentLength) { + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, uri).setBody(body); + httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(contentLength)); + return httpRequest; + } + + @Test + public void userProvidedLengthShouldNotBeTrustedTooLarge() throws IOException { + try (InputStream byteArrayInputStream = new ByteArrayInputStream(EXPECTED)) { + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost") + .setBody(BinaryData.fromStream(byteArrayInputStream, EXPECTED.length - 1L)); + httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length - 1L)); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, + () -> validateAndCollectRequest(httpRequest), "Expected validateLength() to throw, but it didn't"); + assertEquals("Request body emitted " + EXPECTED.length + " bytes, more than the expected " + + (EXPECTED.length - 1) + " bytes.", thrown.getMessage()); + } + } + + @Test + public void userProvidedLengthShouldNotBeTrustedTooSmall() throws IOException { + try (InputStream byteArrayInputStream = new ByteArrayInputStream(EXPECTED)) { + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost") + .setBody(BinaryData.fromStream(byteArrayInputStream, EXPECTED.length + 1L)); + httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length + 1L)); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, + () -> validateAndCollectRequest(httpRequest), "Expected validateLength() to throw, but it didn't"); + + assertEquals("Request body emitted " + EXPECTED.length + " bytes, less than the expected " + + (EXPECTED.length + 1) + " bytes.", thrown.getMessage()); + } + } + + @Test + public void expectedBodyLength() throws IOException { + try (InputStream byteArrayInputStream = new ByteArrayInputStream(EXPECTED)) { + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost") + .setBody(BinaryData.fromStream(byteArrayInputStream, (long) EXPECTED.length)); + httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length)); + + assertArraysEqual(EXPECTED, validateAndCollectRequest(httpRequest)); + } + } + + private static byte[] validateAndCollectRequest(HttpRequest request) { + return RestProxyImpl.validateLength(request).toBytes(); + } } diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/RestProxyUtilsTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/RestProxyUtilsTests.java deleted file mode 100644 index 1861533e08a9b..0000000000000 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/RestProxyUtilsTests.java +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.implementation.http.rest; - -import io.clientcore.core.http.models.HttpHeaderName; -import io.clientcore.core.http.models.HttpMethod; -import io.clientcore.core.http.models.HttpRequest; -import io.clientcore.core.util.binarydata.BinaryData; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Stream; - -import static io.clientcore.core.util.TestUtils.assertArraysEqual; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class RestProxyUtilsTests { - private static final String SAMPLE = "sample"; - private static final byte[] EXPECTED = SAMPLE.getBytes(StandardCharsets.UTF_8); - - @ParameterizedTest - @MethodSource("expectedBodyLengthDataProvider") - public void expectedBodyLength(HttpRequest httpRequest) { - BinaryData binaryData = RestProxyUtils.validateLength(httpRequest); - - assertNotNull(binaryData); - assertArraysEqual(EXPECTED, binaryData.toBytes()); - } - - public static Stream expectedBodyLengthDataProvider() throws Exception { - return dataProvider(EXPECTED.length); - } - - @Test - public void emptyRequestBody() { - HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost"); - - assertNull(RestProxyUtils.validateLength(httpRequest)); - } - - @ParameterizedTest - @MethodSource("unexpectedBodyLengthTooSmallDataProvider") - public void unexpectedBodyLengthTooSmall(HttpRequest httpRequest) { - assertThrows(IllegalStateException.class, () -> validateAndCollectRequest(httpRequest), "Request body " - + "emitted " + EXPECTED.length + " bytes, less than the expected " + (EXPECTED.length + 1) + " bytes."); - } - - public static Stream unexpectedBodyLengthTooSmallDataProvider() throws Exception { - return dataProvider(EXPECTED.length + 1); - } - - @ParameterizedTest - @MethodSource("unexpectedBodyLengthTooLargeDataProvider") - public void unexpectedBodyLengthTooLarge(HttpRequest httpRequest) { - assertThrows(IllegalStateException.class, () -> validateAndCollectRequest(httpRequest), "Request body " - + "emitted " + EXPECTED.length + " bytes, more than the expected " + (EXPECTED.length - 1) + " bytes."); - } - - public static Stream unexpectedBodyLengthTooLargeDataProvider() throws Exception { - return dataProvider(EXPECTED.length - 1); - } - - @Test - public void multipleToBytesToCheckBodyLength() { - HttpRequest httpRequest - = new HttpRequest(HttpMethod.GET, "http://localhost").setBody(BinaryData.fromBytes(EXPECTED)); - httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length)); - - BinaryData binaryData = RestProxyUtils.validateLength(httpRequest); - - assertNotNull(binaryData); - assertArraysEqual(EXPECTED, binaryData.toBytes()); - assertArraysEqual(EXPECTED, binaryData.toBytes()); - } - - private static Stream dataProvider(int contentLength) throws Exception { - Path file = Files.createTempFile(RestProxyUtils.class.getSimpleName(), null); - file.toFile().deleteOnExit(); - Files.write(file, EXPECTED); - - return Stream.of( - Arguments.of(Named.of("bytes", - createHttpRequest("http://localhost", BinaryData.fromBytes(EXPECTED), contentLength))), - Arguments.of(Named.of("string", - createHttpRequest("http://localhost", BinaryData.fromString(SAMPLE), contentLength))), - Arguments.of( - Named.of("stream", createHttpRequest("http://localhost", BinaryData.fromFile(file), contentLength))), - Arguments - .of(Named.of("file", createHttpRequest("http://localhost", BinaryData.fromFile(file), contentLength)))); - } - - private static HttpRequest createHttpRequest(String uri, BinaryData body, int contentLength) { - HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, uri).setBody(body); - httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(contentLength)); - return httpRequest; - } - - @Test - public void userProvidedLengthShouldNotBeTrustedTooLarge() throws IOException { - try (InputStream byteArrayInputStream = new ByteArrayInputStream(EXPECTED)) { - HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost") - .setBody(BinaryData.fromStream(byteArrayInputStream, EXPECTED.length - 1L)); - httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length - 1L)); - - IllegalStateException thrown = assertThrows(IllegalStateException.class, - () -> validateAndCollectRequest(httpRequest), "Expected validateLength() to throw, but it didn't"); - assertEquals("Request body emitted " + EXPECTED.length + " bytes, more than the expected " - + (EXPECTED.length - 1) + " bytes.", thrown.getMessage()); - } - } - - @Test - public void userProvidedLengthShouldNotBeTrustedTooSmall() throws IOException { - try (InputStream byteArrayInputStream = new ByteArrayInputStream(EXPECTED)) { - HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost") - .setBody(BinaryData.fromStream(byteArrayInputStream, EXPECTED.length + 1L)); - httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length + 1L)); - - IllegalStateException thrown = assertThrows(IllegalStateException.class, - () -> validateAndCollectRequest(httpRequest), "Expected validateLength() to throw, but it didn't"); - - assertEquals("Request body emitted " + EXPECTED.length + " bytes, less than the expected " - + (EXPECTED.length + 1) + " bytes.", thrown.getMessage()); - } - } - - @Test - public void expectedBodyLength() throws IOException { - try (InputStream byteArrayInputStream = new ByteArrayInputStream(EXPECTED)) { - HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost") - .setBody(BinaryData.fromStream(byteArrayInputStream, (long) EXPECTED.length)); - httpRequest.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(EXPECTED.length)); - - assertArraysEqual(EXPECTED, validateAndCollectRequest(httpRequest)); - } - } - - private static byte[] validateAndCollectRequest(HttpRequest request) { - return RestProxyUtils.validateLength(request).toBytes(); - } -}