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

Add RequestOptions & handling in RestProxy #22334

6 changes: 6 additions & 0 deletions sdk/core/azure-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@
<version>1.22</version> <!-- {x-version-update;org.openjdk.jmh:jmh-generator-annprocess;external_dependency} -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
<version>1.1.4</version> <!-- {x-version-update;javax.json:javax.json-api;external_dependency} -->
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http;

import com.azure.core.util.BinaryData;

import java.util.function.Consumer;

/**
* This class contains the options to customize a HTTP request. {@link RequestOptions} can be
* used to configure the request headers, query params, the request body, or add a callback
* to modify all aspects of the HTTP request.
*
* <p>
* An instance of fully configured {@link RequestOptions} can be passed to a service method that
* preconfigures known components of the request like URL, path params etc, further modifying both
* un-configured, or preconfigured components.
* </p>
*
* <p>
* To demonstrate how this class can be used to construct a request, let's use a Pet Store service as an example. The
* list of APIs available on this service are <a href="https://petstore.swagger.io/#/pet">documented in the swagger definition.</a>
* </p>
*
* <p><strong>Creating an instance of DynamicRequest using the constructor</strong></p>
* {@codesnippet com.azure.core.experimental.http.requestoptions.instantiation}
**
* <p><strong>Configuring the request with a path param and making a HTTP GET request</strong></p>
* Continuing with the pet store example, getting information about a pet requires making a
* <a href="https://petstore.swagger.io/#/pet/getPetById">HTTP GET call
* to the pet service</a> and setting the pet id in path param as shown in the sample below.
*
* {@codesnippet com.azure.core.experimental.http.dynamicrequest.getrequest}
*
* <p><strong>Configuring the request with JSON body and making a HTTP POST request</strong></p>
* To <a href="https://petstore.swagger.io/#/pet/addPet">add a new pet to the pet store</a>, a HTTP POST call should
* be made to the service with the details of the pet that is to be added. The details of the pet are included as the
* request body in JSON format.
*
* The JSON structure for the request is defined as follows:
* <pre>{@code
* {
* "id": 0,
* "category": {
* "id": 0,
* "name": "string"
* },
* "name": "doggie",
* "photoUrls": [
* "string"
* ],
* "tags": [
* {
* "id": 0,
* "name": "string"
* }
* ],
* "status": "available"
* }
* }</pre>
*
* To create a concrete request, Json builder provided in javax package is used here for demonstration. However, any
* other Json building library can be used to achieve similar results.
*
* {@codesnippet com.azure.core.experimental.http.requestoptions.createjsonrequest}
*
* Now, this string representation of the JSON request can be set as body of RequestOptions
*
* {@codesnippet com.azure.core.experimental.http.requestoptions.postrequest}
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
*/
public class RequestOptions {
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
private Consumer<HttpRequest> requestCallback = request -> { };
private ResponseStatusOption statusOption = ResponseStatusOption.DEFAULT;

/**
* Gets the request callback, applying all the configurations set on this RequestOptions.
* @return the request callback
*/
public Consumer<HttpRequest> getRequestCallback() {
return this.requestCallback;
}

/**
* Gets under what conditions the operation raises an exception if the underlying response indicates a failure.
* @return the configured option
*/
public ResponseStatusOption getStatusOption() {
return this.statusOption;
}

/**
* Adds a header to the HTTP request.
* @param header the header key
* @param value the header value
*
* @return the modified RequestOptions object
*/
public RequestOptions addHeader(String header, String value) {
this.requestCallback = this.requestCallback.andThen(request ->
request.getHeaders().set(header, value));
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
return this;
}

/**
* Adds a query parameter to the request URL.
* @param parameterName the name of the query parameter
* @param value the value of the query parameter
* @return the modified RequestOptions object
*/
public RequestOptions addQueryParam(String parameterName, String value) {
this.requestCallback = this.requestCallback.andThen(request -> {
String url = request.getUrl().toString();
request.setUrl(url + (url.contains("?") ? "&" : "?") + parameterName + "=" + value);
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
});
return this;
}

/**
* Adds a custom request callback to modify the HTTP request before it's sent by the HttpClient.
* The modifications made on a RequestOptions object is applied in order on the request.
*
* @param requestCallback the request callback
* @return the modified RequestOptions object
*/
public RequestOptions addRequestCallback(Consumer<HttpRequest> requestCallback) {
Copy link
Member

Choose a reason for hiding this comment

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

requestCallback may not convey the purpose of the callback. Other options:

  • addRequestModifier
  • addRequestProcessor
  • addRequestHandler
  • addRequestTransformation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sticking with requestCallback for now and may revisit in the future

this.requestCallback = this.requestCallback.andThen(requestCallback);
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
return this;
}

/**
* Sets the body to send as part of the HTTP request.
* @param requestBody the request body data
* @return the modified RequestOptions object
*/
public RequestOptions setBody(BinaryData requestBody) {
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
this.requestCallback = this.requestCallback.andThen(request -> {
request.setBody(requestBody.toBytes());
});
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
return this;
}

/**
* Sets under what conditions the operation raises an exception if the underlying response indicates a failure.
* @param statusOption the option to control under what conditions the operation raises an exception
* @return the modified RequestOptions object
*/
public RequestOptions setStatusOption(ResponseStatusOption statusOption) {
this.statusOption = statusOption;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http;

/**
* ResponseStatusOption controls the behavior of an operation based on the status code of a response.
*/
public enum ResponseStatusOption {
/**
* Indicates that an operation should throw an exception when the response indicates a failure.
*/
DEFAULT,
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved

/**
* Indicates that an operation should not throw an exception when the response indicates a failure.
*/
NO_THROW;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import com.azure.core.http.HttpPipelineBuilder;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.RequestOptions;
import com.azure.core.http.ResponseStatusOption;
import com.azure.core.http.policy.CookiePolicy;
import com.azure.core.http.policy.HttpPipelinePolicy;
import com.azure.core.http.policy.RetryPolicy;
Expand Down Expand Up @@ -134,11 +136,17 @@ public Object invoke(Object proxy, final Method method, Object[] args) {
request.setBody(validateLength(request));
}

RequestOptions options = methodParser.setRequestOptions(args);
if (options != null) {
options.getRequestCallback().accept(request);
}

final Mono<HttpResponse> asyncResponse = send(request, context);

Mono<HttpDecodedResponse> asyncDecodedResponse = this.decoder.decode(asyncResponse, methodParser);

return handleRestReturnType(asyncDecodedResponse, methodParser, methodParser.getReturnType(), context);
return handleRestReturnType(asyncDecodedResponse, methodParser,
methodParser.getReturnType(), context, options);
} catch (IOException e) {
throw logger.logExceptionAsError(Exceptions.propagate(e));
}
Expand Down Expand Up @@ -318,9 +326,9 @@ private HttpRequest configRequest(final HttpRequest request, final SwaggerMethod
}

private Mono<HttpDecodedResponse> ensureExpectedStatus(final Mono<HttpDecodedResponse> asyncDecodedResponse,
final SwaggerMethodParser methodParser) {
final SwaggerMethodParser methodParser, RequestOptions options) {
return asyncDecodedResponse
.flatMap(decodedHttpResponse -> ensureExpectedStatus(decodedHttpResponse, methodParser));
.flatMap(decodedHttpResponse -> ensureExpectedStatus(decodedHttpResponse, methodParser, options));
}

private static Exception instantiateUnexpectedException(final UnexpectedExceptionInformation exception,
Expand Down Expand Up @@ -365,10 +373,11 @@ private static Exception instantiateUnexpectedException(final UnexpectedExceptio
* @return An async-version of the provided decodedResponse.
*/
private Mono<HttpDecodedResponse> ensureExpectedStatus(final HttpDecodedResponse decodedResponse,
final SwaggerMethodParser methodParser) {
final SwaggerMethodParser methodParser, RequestOptions options) {
final int responseStatusCode = decodedResponse.getSourceResponse().getStatusCode();
final Mono<HttpDecodedResponse> asyncResult;
if (!methodParser.isExpectedResponseStatusCode(responseStatusCode)) {
if (!methodParser.isExpectedResponseStatusCode(responseStatusCode)
&& (options == null || options.getStatusOption() != ResponseStatusOption.NO_THROW)) {
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
Mono<byte[]> bodyAsBytes = decodedResponse.getSourceResponse().getBodyAsByteArray();

asyncResult = bodyAsBytes.flatMap((Function<byte[], Mono<HttpDecodedResponse>>) responseContent -> {
Expand Down Expand Up @@ -490,9 +499,10 @@ private Mono<?> handleBodyReturnType(final HttpDecodedResponse response,
private Object handleRestReturnType(final Mono<HttpDecodedResponse> asyncHttpDecodedResponse,
final SwaggerMethodParser methodParser,
final Type returnType,
final Context context) {
final Context context,
final RequestOptions options) {
final Mono<HttpDecodedResponse> asyncExpectedResponse =
ensureExpectedStatus(asyncHttpDecodedResponse, methodParser)
ensureExpectedStatus(asyncHttpDecodedResponse, methodParser, options)
.doOnEach(RestProxy::endTracingSpan)
.contextWrite(reactor.util.context.Context.of("TRACING_CONTEXT", context));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.azure.core.http.HttpHeader;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.RequestOptions;
import com.azure.core.implementation.TypeUtil;
import com.azure.core.implementation.UnixTime;
import com.azure.core.implementation.http.UnexpectedExceptionInformation;
Expand Down Expand Up @@ -347,6 +348,16 @@ public Context setContext(Object[] swaggerMethodArguments) {
return (context != null) ? context : Context.NONE;
}

/**
* Get the {@link RequestOptions} passed into the proxy method.
*
* @param swaggerMethodArguments the arguments passed to the proxy method
* @return the request options
*/
public RequestOptions setRequestOptions(Object[] swaggerMethodArguments) {
return CoreUtils.findFirstOfType(swaggerMethodArguments, RequestOptions.class);
Copy link
Member

Choose a reason for hiding this comment

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

This is an ancillary performance question, does the Java proxy interface pass the Object[] in a consistent order? I'm wondering if there could be an optimization for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AutoRest generated code does, but I'm not sure if azure-core could make the assumption that all code follows the same order.

However, a CoreUtils.findLastOfType() might slightly improve the perf.

}

/**
* Get whether or not the provided response status code is one of the expected status codes for this Swagger
* method.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http;
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved

import com.azure.core.util.BinaryData;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;

/**
* JavaDoc code snippets for {@link RequestOptions}.
*/
public class RequestOptionsJavaDocCodeSnippets {

/**
* Sample to demonstrate how to create an instance of {@link RequestOptions}.
* @return An instance of {@link RequestOptions}.
*/
public RequestOptions createInstance() {
// BEGIN: com.azure.core.experimental.http.requestoptions.instantiation
jianghaolu marked this conversation as resolved.
Show resolved Hide resolved
RequestOptions options = new RequestOptions()
.setBody(BinaryData.fromString("{\"name\":\"Fluffy\"}"))
.addHeader("x-ms-pet-version", "2021-06-01");
// END: com.azure.core.experimental.http.requestoptions.instantiation
return options;
}

/**
* Sample to demonstrate setting the JSON request body in a {@link RequestOptions}.
* @return An instance of {@link RequestOptions}.
*/
public RequestOptions setJsonRequestBodyInRequestOptions() {
// BEGIN: com.azure.core.experimental.http.requestoptions.createjsonrequest
JsonArray photoUrls = Json.createArrayBuilder()
.add("https://imgur.com/pet1")
.add("https://imgur.com/pet2")
.build();

JsonArray tags = Json.createArrayBuilder()
.add(Json.createObjectBuilder()
.add("id", 0)
.add("name", "Labrador")
.build())
.add(Json.createObjectBuilder()
.add("id", 1)
.add("name", "2021")
.build())
.build();

JsonObject requestBody = Json.createObjectBuilder()
.add("id", 0)
.add("name", "foo")
.add("status", "available")
.add("category", Json.createObjectBuilder().add("id", 0).add("name", "dog"))
.add("photoUrls", photoUrls)
.add("tags", tags)
.build();

String requestBodyStr = requestBody.toString();
// END: com.azure.core.experimental.http.requestoptions.createjsonrequest

// BEGIN: com.azure.core.experimental.http.requestoptions.postrequest
RequestOptions options = new RequestOptions()
.addRequestCallback(request -> request
// may already be set if request is created from a client
.setUrl("https://petstore.example.com/pet")
.setHttpMethod(HttpMethod.POST)
.setBody(requestBodyStr)
.setHeader("Content-Type", "application/json"));
// END: com.azure.core.experimental.http.requestoptions.postrequest
return options;
}
}
Loading