Repository: Azure/azure-functions-durable-js
Pull request #435 (2023-01-25 20:15:11)

Make callHttp accept one options object argument
Following the lead of #415 and #429, this PR also switches the `context.df.callHttp()` on orchestration contexts from using explicit arguments to using one options argument for all parameters, following the lead of popular frameworks like `axios`

Before (actual sample from our docs [here](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-http-features?tabs=javascript#managed-identities)):

```TS
const restartResponse = yield context.df.callHttp(
        "POST",
        `https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Compute/virtualMachines/${vmName}/restart?api-version=${apiVersion}`,
        undefined, // no request content
        undefined, // no request headers (besides auth which is handled by the token source)
        tokenSource
);
```

After:

```TS
const restartResponse = yield context.df.callHttp({
        method: "POST",
        url: `https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Compute/virtualMachines/${vmName}/restart?api-version=${apiVersion}`,
        tokenSource,
});
```

Changes that were made in this PR:

* Now accepting an options argument inside of 5 different arguments
* Renamed `uri` to `url`
* Renamed `content` to `body` (I'm assuming that's what "request content" meant?)


If there are other (breaking) changes we want to make to the `callHttp` API, now is the time to call them out. For example:
* Do we want to follow `@azure/functions`'s lead and accept an `undici` `HttpHeaders` type for `headers` parameter instead of an object of strings?
* Do we want to rename the `asynchronousPatternEnabled` argument? It always felt like a confusing name to me, but this is the same the extension uses.


Patch:
diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml
index b267a3f..d3f26c7 100644
--- a/azure-pipelines-release.yml
+++ b/azure-pipelines-release.yml
@@ -16,7 +16,7 @@ resources:
         - pipeline: DurableJSCI
           project: "Azure Functions"
           source: azure-functions-durable-js
-          branch: main
+          branch: v3.x
 
 jobs:
     - job: Release
diff --git a/src/durablehttprequest.ts b/src/durablehttprequest.ts
index 3b9686b..04f0c91 100644
--- a/src/durablehttprequest.ts
+++ b/src/durablehttprequest.ts
@@ -1,4 +1,4 @@
-import { TokenSource } from "./tokensource";
+import { TokenSource } from "./types";
 
 /**
  * Data structure representing a durable HTTP request.
diff --git a/src/durableorchestrationcontext.ts b/src/durableorchestrationcontext.ts
index 05c95e4..2ee589d 100644
--- a/src/durableorchestrationcontext.ts
+++ b/src/durableorchestrationcontext.ts
@@ -1,4 +1,3 @@
-import { TokenSource } from "./tokensource";
 import {
     EntityId,
     RetryOptions,
@@ -33,6 +32,7 @@ import {
 } from "./task";
 import moment = require("moment");
 import { ReplaySchema } from "./replaySchema";
+import { CallHttpOptions } from "./types";
 
 /**
  * Parameter data for orchestration bindings that can be used to schedule
@@ -301,28 +301,24 @@ export class DurableOrchestrationContext {
      *
      * @param req The durable HTTP request to schedule.
      */
-    public callHttp(
-        method: string,
-        uri: string,
-        content?: string | object,
-        headers?: { [key: string]: string },
-        tokenSource?: TokenSource,
-        asynchronousPatternEnabled = true
-    ): Task {
+    public callHttp(options: CallHttpOptions): Task {
+        let content = options.body;
         if (content && typeof content !== "string") {
             content = JSON.stringify(content);
         }
 
-        const req = new DurableHttpRequest(
-            method,
-            uri,
+        const request = new DurableHttpRequest(
+            options.method,
+            options.url,
             content as string,
-            headers,
-            tokenSource,
-            asynchronousPatternEnabled
+            options.headers,
+            options.tokenSource,
+            options.asynchronousPatternEnabled === undefined
+                ? true
+                : options.asynchronousPatternEnabled
         );
-        const newAction = new CallHttpAction(req);
-        if (this.schemaVersion >= ReplaySchema.V3 && req.asynchronousPatternEnabled) {
+        const newAction = new CallHttpAction(request);
+        if (this.schemaVersion >= ReplaySchema.V3 && request.asynchronousPatternEnabled) {
             if (!this.defaultHttpAsyncRequestSleepTimeMillseconds) {
                 throw Error(
                     "A framework-internal error was detected: replay schema version >= V3 is being used, " +
diff --git a/src/tokensource.ts b/src/tokensource.ts
index 648f14e..6b29c56 100644
--- a/src/tokensource.ts
+++ b/src/tokensource.ts
@@ -31,6 +31,3 @@ export class ManagedIdentityTokenSource {
         public readonly resource: string
     ) {}
 }
-
-// Over time we will likely add more implementations
-export type TokenSource = ManagedIdentityTokenSource;
diff --git a/src/types.ts b/src/types.ts
index 3e1a078..f191128 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,6 +7,7 @@ import {
 } from "@azure/functions";
 import { IEntityFunctionContext } from "../src/ientityfunctioncontext";
 import { IOrchestrationFunctionContext } from "../src/iorchestrationfunctioncontext";
+import { ManagedIdentityTokenSource } from "./tokensource";
 
 // orchestrations
 export type OrchestrationHandler = (
@@ -21,6 +22,40 @@ export interface OrchestrationTrigger extends FunctionTrigger {
     type: "orchestrationTrigger";
 }
 
+/**
+ * Options object provided to `callHttp()` methods on orchestration contexts
+ */
+export interface CallHttpOptions {
+    /**
+     * The HTTP request method.
+     */
+    method: string;
+    /**
+     * The HTTP request URL.
+     */
+    url: string;
+    /**
+     * The HTTP request body.
+     */
+    body?: string | object;
+    /**
+     * The HTTP request headers.
+     */
+    headers?: { [key: string]: string };
+    /**
+     * The source of the OAuth token to add to the request.
+     */
+    tokenSource?: TokenSource;
+    /**
+     * Specifies whether the DurableHttpRequest should handle the asynchronous pattern.
+     * @default true
+     */
+    asynchronousPatternEnabled?: boolean;
+}
+
+// Over time we will likely add more implementations
+export type TokenSource = ManagedIdentityTokenSource;
+
 // entities
 export type EntityHandler<T> = (context: IEntityFunctionContext<T>) => void;
 


Test patch:
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index 79efe33..15c3ff1 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -318,14 +318,14 @@ export class TestOrchestrations {
 
     public static SendHttpRequest: any = createOrchestrator(function* (context) {
         const input = context.df.getInput() as df.DurableHttpRequest;
-        const output = yield context.df.callHttp(
-            input.method,
-            input.uri,
-            input.content,
-            input.headers,
-            input.tokenSource,
-            input.asynchronousPatternEnabled
-        );
+        const output = yield context.df.callHttp({
+            method: input.method,
+            url: input.uri,
+            body: input.content,
+            headers: input.headers,
+            tokenSource: input.tokenSource,
+            asynchronousPatternEnabled: input.asynchronousPatternEnabled,
+        });
         return output;
     });