diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/UseReadTransporter.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/UseReadTransporter.java new file mode 100644 index 000000000..21302d269 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/UseReadTransporter.java @@ -0,0 +1,3 @@ +package com.algolia.utils; + +public class UseReadTransporter {} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/retry/RetryStrategy.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/retry/RetryStrategy.java index e7707a5bc..2009c1d16 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/retry/RetryStrategy.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/retry/RetryStrategy.java @@ -1,6 +1,7 @@ package com.algolia.utils.retry; import com.algolia.exceptions.*; +import com.algolia.utils.UseReadTransporter; import com.algolia.utils.Utils; import java.io.IOException; import java.net.SocketTimeoutException; @@ -28,8 +29,11 @@ public Interceptor getRetryInterceptor() { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); + UseReadTransporter useReadTransporter = (UseReadTransporter) request.tag(); Iterator hostsIter = getTryableHosts( - request.method().equals("GET") ? CallType.READ : CallType.WRITE + (useReadTransporter != null || request.method().equals("GET")) + ? CallType.READ + : CallType.WRITE ) .iterator(); while (hostsIter.hasNext()) { diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts index 6193274a4..e2982c0c2 100644 --- a/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts @@ -89,7 +89,7 @@ export function createTransporter({ requestOptions: RequestOptions ): Promise { const stackTrace: StackFrame[] = []; - const isRead = request.method === 'GET'; + const isRead = request.useReadTransporter || request.method === 'GET'; /** * First we prepare the payload that do not depend from hosts. @@ -99,12 +99,13 @@ export function createTransporter({ const method = request.method; // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; + const dataQueryParameters: Record = + request.method === 'GET' + ? { + ...request.data, + ...requestOptions.data, + } + : {}; const queryParameters: QueryParameters = { 'x-algolia-agent': algoliaAgent.value, diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/types/Requester.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/types/Requester.ts index 7ab396431..f15faee70 100644 --- a/clients/algoliasearch-client-javascript/packages/client-common/src/types/Requester.ts +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/types/Requester.ts @@ -7,6 +7,11 @@ export type Request = { path: string; data?: Array> | Record; cacheable?: boolean; + /** + * Some POST methods in the Algolia REST API uses the `read` transporter. + * This information is defined at the spec level. + */ + useReadTransporter?: boolean; }; export type EndRequest = { diff --git a/clients/algoliasearch-client-php/lib/RequestOptions/RequestOptionsFactory.php b/clients/algoliasearch-client-php/lib/RequestOptions/RequestOptionsFactory.php index c271d91b1..7324eac2d 100644 --- a/clients/algoliasearch-client-php/lib/RequestOptions/RequestOptionsFactory.php +++ b/clients/algoliasearch-client-php/lib/RequestOptions/RequestOptionsFactory.php @@ -48,9 +48,10 @@ private function normalize($options) { $normalized = [ 'headers' => [ - 'X-Algolia-Application-Id' => $this->config->getAppId(), - 'X-Algolia-API-Key' => $this->config->getAlgoliaApiKey(), - 'User-Agent' => $this->config->getAlgoliaAgent() !== null + 'x-algolia-application-id' => $this->config->getAppId(), + 'x-algolia-api-key' => $this->config->getAlgoliaApiKey(), + 'User-Agent' => + $this->config->getAlgoliaAgent() !== null ? $this->config->getAlgoliaAgent() : AlgoliaAgent::get(), 'Content-Type' => 'application/json', @@ -61,18 +62,22 @@ private function normalize($options) 'writeTimeout' => $this->config->getWriteTimeout(), 'connectTimeout' => $this->config->getConnectTimeout(), ]; - foreach ($options as $optionName => $value) { if (is_array($value) && $optionName === 'headers') { $headersToLowerCase = []; - foreach ($value as $key => $v) { $headersToLowerCase[mb_strtolower($key)] = $v; } - $normalized[$optionName] = $headersToLowerCase; + $normalized[$optionName] = array_merge( + $normalized[$optionName], + $headersToLowerCase + ); } else { - $normalized[$optionName] = $value; + $normalized[$optionName] = array_merge( + $normalized[$optionName], + $value + ); } } diff --git a/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapper.php b/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapper.php index e3c1c86cf..94ddef938 100644 --- a/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapper.php +++ b/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapper.php @@ -66,30 +66,24 @@ public function __construct( } } - public function read($method, $path, $requestOptions = []) - { - if ('GET' === mb_strtoupper($method)) { + public function sendRequest( + $method, + $path, + $data = [], + $requestOptions = [], + $useReadTransporter = false + ) { + /** + * Some POST methods in the Algolia REST API uses the `read` transporter. + * This information is defined at the spec level. + */ + $isRead = $useReadTransporter || 'GET' === mb_strtoupper($method); + + if ($isRead) { $requestOptions = $this->requestOptionsFactory->createBodyLess( $requestOptions ); - } else { - $requestOptions = $this->requestOptionsFactory->create( - $requestOptions - ); - } - - return $this->request( - $method, - $path, - $requestOptions, - $this->clusterHosts->read(), - $requestOptions->getReadTimeout() - ); - } - - public function write($method, $path, $data = [], $requestOptions = []) - { - if ('DELETE' === mb_strtoupper($method)) { + } elseif ('DELETE' === mb_strtoupper($method)) { $requestOptions = $this->requestOptionsFactory->createBodyLess( $requestOptions ); @@ -104,8 +98,12 @@ public function write($method, $path, $data = [], $requestOptions = []) $method, $path, $requestOptions, - $this->clusterHosts->write(), - $requestOptions->getWriteTimeout(), + $isRead + ? $this->clusterHosts->read() + : $this->clusterHosts->write(), + $isRead + ? $requestOptions->getReadTimeout() + : $requestOptions->getWriteTimeout(), $data ); } diff --git a/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapperInterface.php b/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapperInterface.php index 6c526eae5..224706a0c 100644 --- a/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapperInterface.php +++ b/clients/algoliasearch-client-php/lib/RetryStrategy/ApiWrapperInterface.php @@ -4,9 +4,13 @@ interface ApiWrapperInterface { - public function read($method, $path, $requestOptions = []); - - public function write($method, $path, $data = [], $requestOptions = []); + public function sendRequest( + $method, + $path, + $data = [], + $requestOptions = [], + $useReadTransporter = false + ); public function send($method, $path, $requestOptions = [], $hosts = null); } diff --git a/scripts/playground.ts b/scripts/playground.ts index 6069bb768..af9157d1c 100644 --- a/scripts/playground.ts +++ b/scripts/playground.ts @@ -28,10 +28,10 @@ export async function playground({ break; case 'php': await run( - `cd playground/php && \ + `cd clients/algoliasearch-client-php/ && \ composer update && \ composer dump-autoload && \ - cd src && \ + cd ../../playground/php/src && \ php8 ${client}.php`, { verbose } ); diff --git a/specs/recommend/paths/getRecommendations.yml b/specs/recommend/paths/getRecommendations.yml index 0693bd2f2..369480b29 100644 --- a/specs/recommend/paths/getRecommendations.yml +++ b/specs/recommend/paths/getRecommendations.yml @@ -2,6 +2,7 @@ post: tags: - recommendations operationId: getRecommendations + x-use-read-transporter: true summary: Get results. description: Returns recommendations or trending results, for a specific model and `objectID`. requestBody: diff --git a/specs/search/paths/dictionaries/searchDictionaryEntries.yml b/specs/search/paths/dictionaries/searchDictionaryEntries.yml index 725cf7694..85430f545 100644 --- a/specs/search/paths/dictionaries/searchDictionaryEntries.yml +++ b/specs/search/paths/dictionaries/searchDictionaryEntries.yml @@ -2,6 +2,7 @@ post: tags: - Dictionaries operationId: searchDictionaryEntries + x-use-read-transporter: true description: Search the dictionary entries. summary: Search a dictionary entries. parameters: diff --git a/specs/search/paths/multiclusters/searchUserIds.yml b/specs/search/paths/multiclusters/searchUserIds.yml index 30cc55c97..022d10a07 100644 --- a/specs/search/paths/multiclusters/searchUserIds.yml +++ b/specs/search/paths/multiclusters/searchUserIds.yml @@ -2,6 +2,7 @@ post: tags: - Clusters operationId: searchUserIds + x-use-read-transporter: true summary: Search userID. description: > Search for userIDs. diff --git a/specs/search/paths/objects/multipleGetObjects.yml b/specs/search/paths/objects/multipleGetObjects.yml index 5168a7efd..3fa819b63 100644 --- a/specs/search/paths/objects/multipleGetObjects.yml +++ b/specs/search/paths/objects/multipleGetObjects.yml @@ -2,6 +2,7 @@ post: tags: - Records operationId: getObjects + x-use-read-transporter: true summary: Retrieve one or more objects. description: Retrieve one or more objects, potentially from different indices, in a single API call. requestBody: diff --git a/specs/search/paths/rules/searchRules.yml b/specs/search/paths/rules/searchRules.yml index ff4a4923b..af8aa35d3 100644 --- a/specs/search/paths/rules/searchRules.yml +++ b/specs/search/paths/rules/searchRules.yml @@ -2,6 +2,7 @@ post: tags: - Rules operationId: searchRules + x-use-read-transporter: true summary: Search for rules. description: Search for rules matching various criteria. parameters: diff --git a/specs/search/paths/search/search.yml b/specs/search/paths/search/search.yml index 7aac6b594..76064b625 100644 --- a/specs/search/paths/search/search.yml +++ b/specs/search/paths/search/search.yml @@ -2,6 +2,7 @@ post: tags: - Search operationId: search + x-use-read-transporter: true summary: Search multiple indices. description: Perform a search operation targeting one or many indices. requestBody: diff --git a/specs/search/paths/search/searchForFacetValues.yml b/specs/search/paths/search/searchForFacetValues.yml index afff2acac..f57a81ec1 100644 --- a/specs/search/paths/search/searchForFacetValues.yml +++ b/specs/search/paths/search/searchForFacetValues.yml @@ -2,6 +2,7 @@ post: tags: - Search operationId: searchForFacetValues + x-use-read-transporter: true summary: Search for values of a given facet. description: Search for values of a given facet, optionally restricting the returned values to those contained in objects matching other search criteria. parameters: diff --git a/specs/search/paths/search/searchSingleIndex.yml b/specs/search/paths/search/searchSingleIndex.yml index 104ff5bcd..6aed18cd1 100644 --- a/specs/search/paths/search/searchSingleIndex.yml +++ b/specs/search/paths/search/searchSingleIndex.yml @@ -2,6 +2,7 @@ post: tags: - Search operationId: searchSingleIndex + x-use-read-transporter: true summary: Search in a single index. description: Perform a search operation targeting one specific index. parameters: diff --git a/specs/search/paths/synonyms/searchSynonyms.yml b/specs/search/paths/synonyms/searchSynonyms.yml index b256cadcd..199fb9e8a 100644 --- a/specs/search/paths/synonyms/searchSynonyms.yml +++ b/specs/search/paths/synonyms/searchSynonyms.yml @@ -2,6 +2,7 @@ post: tags: - Synonyms operationId: searchSynonyms + x-use-read-transporter: true summary: Search synonyms. description: Search or browse all synonyms, optionally filtering them by type. parameters: diff --git a/templates/java/libraries/okhttp-gson/ApiClient.mustache b/templates/java/libraries/okhttp-gson/ApiClient.mustache index 5e8ab2abe..42b37b610 100644 --- a/templates/java/libraries/okhttp-gson/ApiClient.mustache +++ b/templates/java/libraries/okhttp-gson/ApiClient.mustache @@ -1,10 +1,7 @@ package {{invokerPackage}}; -import com.algolia.utils.Requester; import com.algolia.exceptions.*; -import com.algolia.utils.AlgoliaAgent; -import com.algolia.utils.JSON; -import com.algolia.utils.RequestOptions; +import com.algolia.utils.*; import okhttp3.*; import okhttp3.internal.http.HttpMethod; @@ -270,11 +267,12 @@ public class ApiClient { * @param body The request body object * @param headerParams The header parameters * @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. + * @param useReadTransporter Some POST methods in the Algolia REST API uses the `read` transporter. This information is defined at the spec level. * @return The HTTP call * @throws AlgoliaRuntimeException If fail to serialize the request body object */ - public Call buildCall(String path, String method, Map queryParameters, Object body, Map headerParams, RequestOptions requestOptions) throws AlgoliaRuntimeException { - Request request = buildRequest(path, method, queryParameters, body, headerParams, requestOptions); + public Call buildCall(String path, String method, Map queryParameters, Object body, Map headerParams, RequestOptions requestOptions, Boolean useReadTransporter) throws AlgoliaRuntimeException { + Request request = buildRequest(path, method, queryParameters, body, headerParams, requestOptions, useReadTransporter); return requester.newCall(request); } @@ -288,39 +286,52 @@ public class ApiClient { * @param body The request body object * @param headerParams The header parameters * @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. + * @param useReadTransporter Some POST methods in the Algolia REST API uses the `read` transporter. This information is defined at the spec level. * @return The HTTP request * @throws AlgoliaRuntimeException If fail to serialize the request body object */ - public Request buildRequest(String path, String method, Map queryParameters, Object body, Map headerParams, RequestOptions requestOptions) throws AlgoliaRuntimeException { - boolean hasRequestOptions = requestOptions != null; - final String url = buildUrl( - path, - queryParameters, - hasRequestOptions ? requestOptions.getExtraQueryParameters() : null - ); - final Request.Builder reqBuilder = new Request.Builder().url(url); - processHeaderParams( - headerParams, - hasRequestOptions ? requestOptions.getExtraHeaders() : null, - reqBuilder - ); + public Request buildRequest( + String path, + String method, + Map queryParameters, + Object body, + Map headerParams, + RequestOptions requestOptions, + Boolean useReadTransporter + ) throws AlgoliaRuntimeException { + boolean hasRequestOptions = requestOptions != null; + final String url = buildUrl( + path, + queryParameters, + hasRequestOptions ? requestOptions.getExtraQueryParameters() : null + ); + final Request.Builder reqBuilder = new Request.Builder().url(url); + processHeaderParams( + headerParams, + hasRequestOptions ? requestOptions.getExtraHeaders() : null, + reqBuilder + ); - RequestBody reqBody; - if (!HttpMethod.permitsRequestBody(method)) { - reqBody = null; - } else if (body == null) { - if ("DELETE".equals(method)) { - // allow calling DELETE without sending a request body - reqBody = null; - } else { - // use an empty request body (for POST, PUT and PATCH) - reqBody = RequestBody.create("", MediaType.parse(this.contentType)); - } - } else { - reqBody = serialize(body); - } + RequestBody reqBody; + if (!HttpMethod.permitsRequestBody(method)) { + reqBody = null; + } else if (body == null) { + if ("DELETE".equals(method)) { + // allow calling DELETE without sending a request body + reqBody = null; + } else { + // use an empty request body (for POST, PUT and PATCH) + reqBody = RequestBody.create("", MediaType.parse(this.contentType)); + } + } else { + reqBody = serialize(body); + } - return reqBuilder.method(method, reqBody).build(); + if (useReadTransporter) { + reqBuilder.tag(new UseReadTransporter()); + } + + return reqBuilder.method(method, reqBody).build(); } /** @@ -401,18 +412,4 @@ public class ApiClient { } } } - - /** - * Build a form-encoding request body with the given form parameters. - * - * @param formParams Form parameters in the form of Map - * @return RequestBody - */ - public RequestBody buildRequestBodyFormEncoding(Map formParams) { - okhttp3.FormBody.Builder formBuilder = new okhttp3.FormBody.Builder(); - for (Entry param : formParams.entrySet()) { - formBuilder.add(param.getKey(), parameterToString(param.getValue())); - } - return formBuilder.build(); - } } diff --git a/templates/java/libraries/okhttp-gson/api.mustache b/templates/java/libraries/okhttp-gson/api.mustache index df3ba1dc3..28e0bb76b 100644 --- a/templates/java/libraries/okhttp-gson/api.mustache +++ b/templates/java/libraries/okhttp-gson/api.mustache @@ -193,7 +193,7 @@ public class {{classname}} extends ApiClient { } {{/headerParams}} - Call call = this.buildCall(requestPath, "{{httpMethod}}", queryParameters, bodyObj, headers, requestOptions); + Call call = this.buildCall(requestPath, "{{httpMethod}}", queryParameters, bodyObj, headers, requestOptions, {{#vendorExtensions.x-use-read-transporter}}true{{/vendorExtensions.x-use-read-transporter}}{{^vendorExtensions.x-use-read-transporter}}false{{/vendorExtensions.x-use-read-transporter}}); Type returnType = new TypeToken<{{{returnType}}}>() {}.getType(); return this.executeAsync(call, returnType); } diff --git a/templates/javascript/api-single.mustache b/templates/javascript/api-single.mustache index 3ef4f80e2..a807a4fd9 100644 --- a/templates/javascript/api-single.mustache +++ b/templates/javascript/api-single.mustache @@ -218,6 +218,9 @@ export function create{{capitalizedApiName}}(options: CreateClientOptions{{#hasR {{#bodyParam}} data: {{paramName}}, {{/bodyParam}} + {{#vendorExtensions.x-use-read-transporter}} + useReadTransporter: true, + {{/vendorExtensions.x-use-read-transporter}} }; return transporter.request(request, { diff --git a/templates/php/Configuration.mustache b/templates/php/Configuration.mustache index 91502981b..61f0bf616 100644 --- a/templates/php/Configuration.mustache +++ b/templates/php/Configuration.mustache @@ -58,12 +58,12 @@ abstract class Configuration { if(isset($config['apiKey'])) { $this->setAlgoliaApiKey($config['apiKey']); - $this->setApiKey('X-Algolia-API-Key', $config['apiKey']); + $this->setApiKey('x-algolia-api-key', $config['apiKey']); } if(isset($config['appId'])) { $this->setAppId($config['appId']); - $this->setApiKey('X-Algolia-Application-Id', $config['appId']); + $this->setApiKey('x-algolia-application-id', $config['appId']); } $config += $this->getDefaultConfiguration(); diff --git a/templates/php/api.mustache b/templates/php/api.mustache index 413131d69..ad10cd9d9 100644 --- a/templates/php/api.mustache +++ b/templates/php/api.mustache @@ -35,7 +35,6 @@ use {{invokerPackage}}\RetryStrategy\ClusterHosts; public function __construct(ApiWrapperInterface $apiWrapper, {{configClassname}} $config) { $this->config = $config; - $this->api = $apiWrapper; } @@ -261,12 +260,12 @@ use {{invokerPackage}}\RetryStrategy\ClusterHosts; {{/servers.0}} - return $this->sendRequest('{{httpMethod}}', $resourcePath, $headers, $queryParameters, $httpBody, $requestOptions); + return $this->sendRequest('{{httpMethod}}', $resourcePath, $headers, $queryParameters, $httpBody, $requestOptions, {{#vendorExtensions.x-use-read-transporter}}true{{/vendorExtensions.x-use-read-transporter}}); } {{/operation}} - private function sendRequest($method, $resourcePath, $headers, $queryParameters, $httpBody, $requestOptions) + private function sendRequest($method, $resourcePath, $headers, $queryParameters, $httpBody, $requestOptions, $useReadTransporter = false) { if (!isset($requestOptions['headers'])) { $requestOptions['headers'] = []; @@ -277,25 +276,15 @@ use {{invokerPackage}}\RetryStrategy\ClusterHosts; $requestOptions['headers'] = array_merge($headers, $requestOptions['headers']); $requestOptions['queryParameters'] = array_merge($queryParameters, $requestOptions['queryParameters']); - $query = \GuzzleHttp\Psr7\Query::build($requestOptions['queryParameters']); - if($method == 'GET') { - $request = $this->api->read( - $method, - $resourcePath . ($query ? "?{$query}" : ''), - $requestOptions - ); - } else { - $request = $this->api->write( - $method, - $resourcePath . ($query ? "?{$query}" : ''), - $httpBody, - $requestOptions - ); - } - - return $request; + return $this->api->sendRequest( + $method, + $resourcePath . ($query ? "?{$query}" : ''), + $httpBody, + $requestOptions, + $useReadTransporter + ); } } {{/operations}} diff --git a/tests/CTS/client/recommend/api.json b/tests/CTS/client/recommend/api.json index ae23150e0..038ffa8d1 100644 --- a/tests/CTS/client/recommend/api.json +++ b/tests/CTS/client/recommend/api.json @@ -14,7 +14,7 @@ "expected": { "match": { "objectContaining": { - "host": "test-app-id.algolia.net" + "host": "test-app-id-dsn.algolia.net" } } } @@ -58,7 +58,7 @@ "match": { "objectContaining": { "connectTimeout": 2, - "responseTimeout": 30 + "responseTimeout": 5 } } } diff --git a/tests/CTS/client/search/api.json b/tests/CTS/client/search/api.json index daf1358dc..0b2c331e2 100644 --- a/tests/CTS/client/search/api.json +++ b/tests/CTS/client/search/api.json @@ -18,7 +18,7 @@ "expected": { "match": { "objectContaining": { - "host": "test-app-id.algolia.net" + "host": "test-app-id-dsn.algolia.net" } } } @@ -70,7 +70,7 @@ "match": { "objectContaining": { "connectTimeout": 2, - "responseTimeout": 30 + "responseTimeout": 5 } } }