Skip to content

feat(durable-functions): restore worker-side callHttp (#318)#333

Open
YunchuWang wants to merge 2 commits into
mainfrom
yunchuwang-restore-callhttp-v4
Open

feat(durable-functions): restore worker-side callHttp (#318)#333
YunchuWang wants to merge 2 commits into
mainfrom
yunchuwang-restore-callhttp-v4

Conversation

@YunchuWang

Copy link
Copy Markdown
Member

What & why

Restores the classic durable-functions v3 context.df.callHttp API on the v4 gRPC-based provider (packages/azure-functions-durable), which currently throws. Fixes #318.

The durabletask gRPC engine has no native durable-HTTP action, so the feature is rebuilt from core primitives. This ports Andy Staples' design from the sibling durabletask-python#155 (JS counterpart of #282).

Ported design (mirrors Python builtin.py)

  • Built-in HTTP activity (BuiltIn__HttpActivity): performs a single fetch request and returns { statusCode, headers, content }. Non-2xx/202 responses are captured, not thrown (global fetch only rejects on network errors). Includes an SSRF guard rejecting any non-http/https scheme, and acquires a Managed Identity bearer token when a tokenSource is supplied.
  • Built-in poll orchestrator (BuiltIn__HttpPollOrchestrator): a core-native async function* that calls the activity and, while the response is 202 Accepted with a Location header, waits on a replay-safe durable timer (fireAt = currentUtcDateTime + delay, never Date.now()) honoring Retry-After (delta-seconds and HTTP-date forms; 1s fallback), resolves a relative Location against the current URI, and re-polls. Honors the v3 enablePolling opt-out.
  • callHttp(options): builds the request payload and schedules the poll orchestrator via callSubOrchestrator — a single yield, durable polling — returning Task<DurableHttpResponse>.
  • Auto-registration: both builtins register exactly once when the package is imported, so existing apps work with no changes.

v3 API restored

Re-exports the v3 public shapes so v3 imports resolve unchanged: DurableHttpRequest, DurableHttpResponse, CallHttpOptions, ManagedIdentityTokenSource, TokenSource. callHttp accepts the v3 CallHttpOptions (method, url, body, headers, tokenSource, enablePolling, deprecated asynchronousPatternEnabled alias), JSON-stringifies an object body, and resolves enablePolling = enablePolling ?? asynchronousPatternEnabled ?? true.

Note: ManagedIdentityTokenSource.kind is "AzureManagedIdentity" to match the actual v3 source shape (the local azure-functions-durable-js clone), rather than "ManagedIdentity". Only resource is functionally significant.

⚠️ Trust-boundary change (v3 → v4)

In v3, the Functions host extension executed the HTTP request. In v4, it runs as a durable activity inside the app/worker process (via fetch). Therefore the egress network path, outbound identity, and firewall/VNet behavior follow the worker process, not the host — re-verify egress and any IP allow-lists. This is documented in the README migration section.

Managed identity (optional dependency)

Token acquisition uses @azure/identity DefaultAzureCredential, loaded via a lazy require only when a tokenSource is present (scope = resource.replace(/\/+$/,'') + '/.default'; sends the real Bearer <token>). It is added as an optional dependency; if a tokenSource is used without the package installed, a clear "install @azure/identity" error is thrown. Apps that don't use managed identity need no extra install.

Tests

  • Built-in activity: 200 passthrough; missing uri throws; non-http/https scheme throws; tokenSource sets a real Authorization: Bearer <token> (credential mocked; asserts the real token); 202 captured (not thrown).
  • Poll orchestrator: single 200 returns immediately (no timer); 202+Location yields activity → timer → re-poll; Retry-After delta-seconds and HTTP-date parsing; relative Location resolution; enablePolling=false returns the first 202 without looping.
  • callHttp wiring: schedules the sub-orchestration under BUILTIN_HTTP_POLL_ORCHESTRATOR_NAME with the built payload; object body → JSON.stringify; default enablePolling true. Updated the existing orchestration-context.spec.ts test that previously asserted callHttp throws.
  • Auto-registration: both builtins are registered via the app registration path.

Gates

  • npm run build:core — exit 0
  • npm run build -w durable-functions — exit 0
  • npm run lint — exit 0
  • durable-functions unit tests — 125 passed / 17 suites
  • core unit tests — 1163 passed / 66 suites

Credit: ported from durabletask-python#155 (Andy Staples). CHANGELOG intentionally left unchanged.

Fixes #318

Restore the classic durable-functions v3 `context.df.callHttp` API on the
v4 gRPC-based provider. The durabletask gRPC engine has no native
durable-HTTP action, so the feature is reconstructed from core primitives,
porting Andy Staples' durabletask-python#155 design:

- Built-in HTTP activity (`BuiltIn__HttpActivity`): performs one `fetch`
  request, captures non-2xx/202 responses instead of throwing, guards
  against non-http(s) schemes (SSRF), and acquires a Managed Identity
  bearer token via the optional `@azure/identity` package when a
  tokenSource is supplied.
- Built-in poll orchestrator (`BuiltIn__HttpPollOrchestrator`): a
  core-native async generator that re-polls while the endpoint returns
  `202 Accepted` with a `Location`, waiting on replay-safe durable timers
  that honor `Retry-After` (delta-seconds and HTTP-date forms), resolving
  relative Locations, and honoring the v3 `enablePolling` opt-out.
- `callHttp(options)` builds the request and schedules the poll
  orchestrator as a sub-orchestration (single yield), returning
  `Task<DurableHttpResponse>`.
- Both builtins auto-register once when the package is imported.

Also restores the v3 public types (`DurableHttpRequest`,
`DurableHttpResponse`, `CallHttpOptions`, `ManagedIdentityTokenSource`,
`TokenSource`) and documents the trust-boundary change: the HTTP request
now runs as a durable activity in the app/worker process (via `fetch`),
not in the Functions host extension, so egress path, outbound identity,
and firewall/VNet behavior differ from v3. Managed identity requires the
optional `@azure/identity` package.

Fixes #318

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1120c1a-9b20-4aef-b34a-f7986dd06382
Copilot AI review requested due to automatic review settings July 24, 2026 16:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Restores the classic durable-functions v3 context.df.callHttp API for the durable-functions v4 Azure Functions compatibility package by rebuilding “durable HTTP” on top of durabletask gRPC core primitives (built-in activity + polling sub-orchestrator), including optional Managed Identity token acquisition and replay-safe 202 polling semantics.

Changes:

  • Implemented built-in durable HTTP activity + polling orchestrator and wired DurableOrchestrationContext.callHttp() to schedule it via callSubOrchestrator.
  • Added v3-compatible durable HTTP request/response/token models and re-exported them from the package entrypoint.
  • Added unit tests for HTTP behavior, polling, and import-time auto-registration; updated README and added @azure/identity as an optional dependency.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/azure-functions-durable/src/http/builtin.ts Adds built-in HTTP activity (fetch) and replay-safe 202 polling orchestrator; optional Managed Identity token acquisition.
packages/azure-functions-durable/src/http/models.ts Introduces v3-compatible durable HTTP request/response/token models and internal payload shape.
packages/azure-functions-durable/src/orchestration-context.ts Replaces the previous throwing callHttp implementation with sub-orchestrator scheduling and request payload building.
packages/azure-functions-durable/src/app.ts Auto-registers the built-in HTTP orchestrator/activity on module import.
packages/azure-functions-durable/src/index.ts Re-exports durable HTTP models to preserve v3 import compatibility.
packages/azure-functions-durable/test/unit/http-builtin.spec.ts Adds unit tests for HTTP activity, token acquisition path, and polling/orchestrator behavior.
packages/azure-functions-durable/test/unit/http-builtin-registration.spec.ts Adds tests asserting import-time auto-registration into the Functions app and shared worker registry.
packages/azure-functions-durable/test/unit/orchestration-context.spec.ts Updates tests to assert callHttp scheduling behavior instead of throwing.
packages/azure-functions-durable/README.md Updates migration notes to reflect restored callHttp and documents the trust-boundary change + optional identity dependency.
packages/azure-functions-durable/package.json Adds @azure/identity as an optional dependency.
package-lock.json Updates lockfile for optional dependency addition and workspace metadata.

Comment on lines +94 to +101
try {
identity = require("@azure/identity");
} catch {
throw new Error(
"callHttp with a tokenSource requires the optional '@azure/identity' package. " +
"Install it with `npm install @azure/identity`.",
);
}
Comment on lines +143 to +148
if (resource) {
const token = await acquireBearerToken(resource);
if (headers["Authorization"] === undefined) {
headers["Authorization"] = `Bearer ${token}`;
}
}
Add end-to-end callHttp coverage to the gated Functions-host suite
(test/e2e-functions). The suite previously had zero callHttp coverage;
this drives the restored context.df.callHttp API through a real func
host + Azurite over three modes:

- sync: callHttp -> 200 passthrough (HttpEcho).
- polling: callHttp follows a 202 -> Location poll loop to a final 200
  (HttpAsyncEcho, stateless attempt-encoded 202 so it is deterministic
  across worker processes).
- nopoll: enablePolling:false returns the first 202 without looping.

The test-app functions target the app's own loopback endpoints so the
suite stays hermetic (no external network).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1120c1a-9b20-4aef-b34a-f7986dd06382
Copilot AI review requested due to automatic review settings July 24, 2026 23:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

packages/azure-functions-durable/src/http/builtin.ts:98

  • The require('@azure/identity') error handler catches all failures and always throws an "install @azure/identity" message. This will mask real runtime errors inside the dependency (or other require-time failures) and make diagnosing production issues harder. Only replace the error when the module is actually missing (MODULE_NOT_FOUND); otherwise rethrow the original error.
  try {
    identity = require("@azure/identity");
  } catch {
    throw new Error(
      "callHttp with a tokenSource requires the optional '@azure/identity' package. " +

packages/azure-functions-durable/src/http/builtin.ts:145

  • Managed-identity token injection only checks headers["Authorization"], so a caller-supplied authorization (lowercase) header will be overwritten and/or duplicated. Since header names are case-insensitive, this can unexpectedly change the request's auth behavior.
  const headers: { [key: string]: string } = { ...(request.headers ?? {}) };
  const resource = request.tokenSource?.resource;
  if (resource) {
    const token = await acquireBearerToken(resource);
    if (headers["Authorization"] === undefined) {

Comment on lines +47 to +59
- **`context.df.callHttp(...)` is restored** as a worker-side durable HTTP call
([#318](https://github.com/microsoft/durabletask-js/issues/318)). It accepts the v3
`CallHttpOptions` (`method`, `url`, `body`, `headers`, `tokenSource`, `enablePolling`) and returns a
`Task<DurableHttpResponse>` (`{ statusCode, headers, content }`), including automatic `202 Accepted`
polling that honors `Retry-After` via durable timers. **Trust-boundary change:** in v3 the Functions
**host** extension executed the HTTP request; here it runs as a durable **activity inside your
app/worker process** (via `fetch`). Outbound network path, source identity, and firewall/VNet rules
therefore follow the worker process, not the host — re-verify egress and any IP allow-lists. A
managed-identity `tokenSource` requires the optional
[`@azure/identity`](https://www.npmjs.com/package/@azure/identity) package
(`npm install @azure/identity`); without it, a request that uses a `tokenSource` throws a clear error.
- **Some v3 top-level exports were removed** — `DummyOrchestrationContext` / `DummyEntityContext`
(testing utilities), `ManagedIdentityTokenSource`, and the entity-lock types above. `TaskFailedError`
(testing utilities) and the entity-lock types above. `TaskFailedError`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

make this more succinct and customer facing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[durable-functions v4] Investigate restoring context.df.callHttp as a worker-side durable activity

2 participants