Skip to content

`core http` dependency migration to `core client` `core rest pipeline`

Jonathan Cárdenas edited this page May 3, 2022 · 7 revisions

The @azure/search-documents has been migrated from @azure/core-http dependency to @azure/core-client/@azure/core-rest-pipeline packages. The PR for the same is available at #17872. The steps and details for the migration have been documented here.

Note: These steps are specific to one migration and might not consist of all possible scenarios.

Generation

In the swagger configuration file, you need to use the correct extension. For example, the code:

use-extension:
  "@autorest/typescript": "6.0.0-beta.4"

This should be modified as:

use-extension:
  "@autorest/typescript": "6.0.0-beta.13"

You can find the latest version to be used at npmjs. The use-core-v2 property must be changed from false to true.

Modifying package.json file

Remove the dependency on @azure/core-http and make a dependency on @azure/core-client & @azure/core-rest-pipeline packages.

Import Changes

  • createPipelineFromOptions - Need not be used after the migration
  • InternalPipelineOptions - Change it to InternalClientPipelineOptions
  • allowedHeaderNames - Change it to additionalAllowedHeaderNames
  • RequestPolicyFactory - Need not be used after the migration
  • PipelineOptions - Change it to CommonClientOptions
  • bearerTokenAuthenticationPolicy - Change it to use bearerTokenAuthenticationPolicy from core-rest-pipeline
  • RestError - Change it to use RestError from core-rest-pipeline
  • OperationOptions - Change it to use OperationOptions from core-client
  • delay - Import it from core-util
  • isNode - Import it from core-util
  • WebResourceLike - Change it to PipelineRequest from core-rest-pipeline
  • HttpOperationResponse - Change it to PipelineResponse from core-rest-pipeline
  • RequestOptionsBase - Change it to OperationOptions
  • DefaultHttpClient - Change it to createDefaultHttpClient from core-rest-pipeline

Note: core-util is still in beta, use a local copy instead if there is a conflict with the package

Imports for creating policies

The following imports should be removed while creating the policies.

  • RequestPolicy - Need not be used after the migration
  • BaseRequestPolicy - Need not be used after the migration
  • HttpOperationResponse - Need not be used after the migration
  • RequestPolicyOptionsLike - Need not be used after the migration

Instead, the following imports (from core-rest-pipeline) should be used: PipelinePolicy,PipelineRequest,SendRequest & PipelineResponse.

Constructor Changes

Remove the code to handle userAgentOptions

In the constructor of the custom client, if there is any code (similar to the one below) to create and pass userAgentOptions to GeneratedClient, it should be removed. The userAgentOptions is now handled by GeneratedClient automatically.

    const libInfo = `azsdk-js-<service name>/${SDK_VERSION}`;
    if (!options.userAgentOptions) {
      options.userAgentOptions = {};
    }
    if (options.userAgentOptions.userAgentPrefix) {
      options.userAgentOptions.userAgentPrefix = `${options.userAgentOptions.userAgentPrefix} ${libInfo}`;
    } else {
      options.userAgentOptions.userAgentPrefix = libInfo;
    }

Please DO verify that proper telemetry information is set in the HTTP headers when the SDK is sending out requests.

Pipeline Changes

While using the core-http, we create a pipeline using createPipelineFromOptions. This pipeline is passed to the constructor of the GenerateClient. The code looks like:

const pipeline = createPipelineFromOptions(internalPipelineOptions, requestPolicyFactory);
this.client = new GeneratedClient(this.endpoint, apiVersion, pipeline);

As mentioned in the Import Changes section, createPipelineFromOptions import has been removed. Now, we could pass the internalClientPipelineOptions directly to the constructor of the genereate client. The code looks like:

this.client = new GeneratedClient(this.endpoint, apiVersion, internalClientPipelineOptions);

RequestPolicyFactory Changes

While using the core-http, we create a RequestPolicyFactory object (before creating an instance of the GeneratedClient) and add or remove policies to it. The code looks like:

const requestPolicyFactory: RequestPolicyFactory = isTokenCredential(credential)
      ? bearerTokenAuthenticationPolicy(credential, utils.DEFAULT_SEARCH_SCOPE)
      : createSearchApiKeyCredentialPolicy(credential);

const pipeline = createPipelineFromOptions(internalPipelineOptions, requestPolicyFactory);

if (Array.isArray(pipeline.requestPolicyFactories)) {
   pipeline.requestPolicyFactories.unshift(odataMetadataPolicy("minimal"));
}

With the migration, the RequestPolicyFactory has been removed. The policies should be added/removed AFTER the creating an instance of the GeneratedClient. The code looks like:

this.client = new GeneratedClient(this.endpoint, apiVersion, internalClientPipelineOptions);

if (isTokenCredential(credential)) {
   this.client.pipeline.addPolicy(
      bearerTokenAuthenticationPolicy({ credential, scopes: utils.DEFAULT_SEARCH_SCOPE })
   );
} else {
   this.client.pipeline.addPolicy(createSearchApiKeyCredentialPolicy(credential));
}

this.client.pipeline.addPolicy(createOdataMetadataPolicy("minimal"));

Method Changes

Removal of operationOptionsToRequestOptionsBase

While using the core-http, we call the method operationOptionsToRequestOptionsBase with the user passed options, before calling the generated methods. This is done to ensure the options are modified to specific structure (of RequestOptions). The code looks like:

public async listSynonymMaps(options: ListSynonymMapsOptions = {}): Promise<Array<SynonymMap>> {
    const { span, updatedOptions } = createSpan("SearchIndexClient-listSynonymMaps", options);
    try {
      const result = await this.client.synonymMaps.list(operationOptionsToRequestOptionsBase(updatedOptions));

This requirement of changing the options to specific structure has been removed with the migration. Now, it can be passed directly. The code looks like:

public async listSynonymMaps(options: ListSynonymMapsOptions = {}): Promise<Array<SynonymMap>> {
    const { span, updatedOptions } = createSpan("SearchIndexClient-listSynonymMaps", options);
    try {
      const result = await this.client.synonymMaps.list(updatedOptions);

Change to the _response property

While using the core-http, there are times where we read _response property (in both src and test code). The code looks like:

public async indexDocuments(batch: IndexDocumentsBatch<T>, options: IndexDocumentsOptions = {}): Promise<IndexDocumentsResult> {
    const { span, updatedOptions } = createSpan("SearchClient-indexDocuments", options);
    try {
      const result = await this.client.documents.index(
        { actions: serialize(batch.actions) },
        operationOptionsToRequestOptionsBase(updatedOptions)
      );
      if (options.throwOnAnyFailure && result._response.status === 207) {

With the migration, the _response has been removed. We should access the value using the onResponse method. The code looks like:

public async indexDocuments(batch: IndexDocumentsBatch<T>, options: IndexDocumentsOptions = {}): Promise<IndexDocumentsResult> {
    const { span, updatedOptions } = createSpan("SearchClient-indexDocuments", options);
    try {
      let status: number = 0;
      const result = await this.client.documents.index(
        { actions: serialize(batch.actions) },
        {
          ...updatedOptions,
          onResponse: (response) => {
            status = response.status;
          }
        }
      );
      if (options.throwOnAnyFailure && status === 207) {

Changes to mock clients that return a response for unit tests

Sometimes, there are tests that use mock clients with error responses just for the sake of the test. Usually, those clients would override a method by returning a _response, take this example from KeyVault:

it("403 doesn't throw", async function () {
      const code = 403;
      const client: any = {
        async recoverDeletedCertificate(): Promise<any> {
          return {
            _response: {
              bodyAsText: JSON.stringify({
            id: "/version/name/version",
            recoveryId: "something",
              }),
            },
          };
        },
        async getCertificate(): Promise<any> {
          throw new RestError(`${code}`, { statusCode: code });
        },
      };

      [...]

    });

Since the _response property is no longer used in CoreV2, instead of returning an object the fake client should create a response and call .onResponse. So you'll end up with something like this:

it("403 doesn't throw", async function () {
      const code = 403;
      const fooClient: Partial<KeyVaultClient> = {
        async recoverDeletedCertificate(_a,_b,c): Promise<any> {
          const request: PipelineRequest = {url: "", method: "GET", headers: createHttpHeaders(), timeout: 100, withCredentials: false, requestId: "something"};
          const body = {
            id: "/version/name/version",
            recoveryId: "something",
          }
          const response : FullOperationResponse = {
            request: request,
            bodyAsText: JSON.stringify(body),
            status: code,
            headers: createHttpHeaders(),
            parsedBody: body,
          };
          c?.onResponse && c.onResponse(response, response , {});
        },

        async getCertificate(): Promise<any> {
          throw new RestError(`${code}`, { statusCode: code });
        },
      };

      [...]

    });

Need of Extract property

While using the core-http, there are places where you could use Fields extend keyof T. The code looks like:

public async getDocument<Fields extends keyof T>(key: string, options: GetDocumentOptions<Fields> = {}): Promise<T> {}

With the migration, it has to be modified with Extract property. The code should be modified as:

public async getDocument<Fields extends Extract<keyof T, string>>(key: string, options: GetDocumentOptions<Fields> = {}): Promise<T> {

Creating custom policies

While using the core-http, we use custom class that extends BaseRequestPolicy with a sendRequest method (with webResource parameter). The code looks like:

export function createSearchApiKeyCredentialPolicy(
  credential: KeyCredential
): RequestPolicyFactory {
  return {
    create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
      return new SearchApiKeyCredentialPolicy(nextPolicy, options, credential);
    }
  };
}

/**
 * A concrete implementation of an AzureKeyCredential policy
 * using the appropriate header for Azure Cognitive Search
 */
class SearchApiKeyCredentialPolicy extends BaseRequestPolicy {
  private credential: KeyCredential;

  constructor(
    nextPolicy: RequestPolicy,
    options: RequestPolicyOptionsLike,
    credential: KeyCredential
  ) {
    super(nextPolicy, options);
    this.credential = credential;
  }

  public async sendRequest(webResource: WebResourceLike): Promise<HttpOperationResponse> {
    if (!webResource) {
      throw new Error("webResource cannot be null or undefined");
    }

    webResource.headers.set(API_KEY_HEADER_NAME, this.credential.key);
    return this._nextPolicy.sendRequest(webResource);
  }
}

This has been simplified in the core-rest-pipeline model. You could create the custom policy like this:

const searchApiKeyCredentialPolicy = "SearchApiKeyCredentialPolicy";

export function createSearchApiKeyCredentialPolicy(credential: KeyCredential): PipelinePolicy {
  return {
    name: searchApiKeyCredentialPolicy,
    async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
      if (!request.headers.has(API_KEY_HEADER_NAME)) {
        request.headers.set(API_KEY_HEADER_NAME, credential.key);
      }
      return next(request);
    }
  };
}

Troubleshooting

Update test recordings

It is possible that unit tests will start failing after doing the migration, which could be caused by the test recordings not being up to date. Consider re-recording the tests after doing the generation and migration. More information on the new test recorder can be found in this document.

Tests not passing due to a Fetch error

Old core package used to depend on XhrHttpRequest, which is no longer needed in Core V2. However, old recorder still uses it, so it's a good idea to migrate the recorder to the new one when you are doing this migration. If you are facing a fetch error when running your tests, you have to options:

  1. Migrate to the new recorder, here's a detailed guide for this. -or-
  2. Update your test client to include an XhrHttpClient in the options: Import createXhrHttpClient from test-utils. Then, in the test client definition add an httpClient option that includes createXhrHttpClient only for browser tests. Here's an example:
const client = new SecretClient(keyVaultUrl, credential, {
  serviceVersion,
  httpClient: isNode ? undefined : createXhrHttpClient(),
});

Requests failing due to a duplication of the Path in the URL

If you encounter an error like:

Secret /deletetSecrets was not found...

Take a look at the request's URL. If the URL has the path duplicated, like https://mykeyvault.vault.azure.net/deletedsecrets/deletedsecrets?api-version... this is caused by the iterator being in in the convienence layer, so you will need to make changes in there.

Any method call that passes a continuationToken should be replaced by a method of the same name with the sufix Next. For example: getDeletedSecret(continuationToken, options ) should be changed to getDeletedSecretNext(vaultURI, continuationToken, options).

Another solution is to generate the client with the flag disable-async-iterators set to false, this will generate the async iterators and there will be no need for them in the convenience layer.

Other changes to consider when migrating to Core V2

Metadata in Package.json

The sdk bot uses metadata in the package.json to automatically upgrade the package version in the code after each release, like in this PR. Here is an example of how the metadata section looks in the Mixed Reality package.json:

"//metadata": {
    "constantPaths": [
      {
        "path": "src/generated/mixedRealityStsRestClientContext.ts",
        "prefix": "packageVersion"
      },
      {
        "path": "src/constants.ts",
        "prefix": "SDK_VERSION"
      },
      {
        "path": "swagger/README.md",
        "prefix": "package-version"
      }
    ]
  },

When doing the minor version bump, also check if all paths are covering all the constants where the package version is being specified and add any missing constant path to the array.

Local copy of `isNode`

Create a new file isNode.ts and import isNode from it, in that file add the following code:

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
 * A constant that indicates whether the environment the code is running is Node.JS.
 */
export const isNode =
  typeof process !== "undefined" && Boolean(process.version) && Boolean(process.versions?.node);

If the packages support browser

In that case, another file named isNode.browser.ts is needed. Copy the following into that file:

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
 * A constant that indicates whether the environment the code is running is Node.JS.
 */
export const isNode = false;

Next, add a browser mapping in the package.json, similar to this example from Core:

browser": { 
   "./dist-esm/src/isNode.js": "./dist-esm/src/isNode.browser.js" 
 },