Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6267ba7
feat(durable-functions): restore worker-side callHttp (#318)
YunchuWang Jul 24, 2026
81c6000
test(e2e-functions): add callHttp host e2e (sync/polling/no-poll)
YunchuWang Jul 24, 2026
ad3ae85
fix(azure-functions-durable): harden callHttp 202 poll (cross-origin …
YunchuWang Jul 27, 2026
5015da6
test(azure-functions-durable): add direct-invocation e2e for callHttp…
YunchuWang Jul 27, 2026
e409567
fix(azure-functions-durable): use the original request URI as the cal…
YunchuWang Jul 27, 2026
dbc5f47
Fix default sub-orchestration instance ID collision across continue-a…
YunchuWang Jul 27, 2026
5961a40
test(e2e-azuremanaged): DTS regression for sub-orch ID collision acro…
YunchuWang Jul 27, 2026
2a71dc6
chore: revert CHANGELOG.md entry per maintainer preference
YunchuWang Jul 27, 2026
074d419
fix(core): key default sub-orchestration instance ID on executionId o…
YunchuWang Jul 27, 2026
81d51ac
test: assert the exact `${executionId}:${hex4}` shape for default sub…
YunchuWang Jul 27, 2026
fb5ad66
fix(azure-functions-durable): rethrow non-MODULE_NOT_FOUND errors fro…
YunchuWang Jul 27, 2026
b8132e8
fix(azure-functions-durable): guard malformed poll Location and make …
YunchuWang Jul 27, 2026
6f9ced9
fix(azure-functions-durable): cache identity credential + harden test…
YunchuWang Jul 27, 2026
29ddc34
docs(azure-functions-durable): sharpen credential-cache rationale in …
YunchuWang Jul 27, 2026
e1c6226
docs(azure-functions-durable): align credential-cache test comment wi…
YunchuWang Jul 27, 2026
4d2709c
fix(azure-functions-durable): stop polling on a non-http(s) 202 Location
YunchuWang Jul 27, 2026
c79b77a
Align callHttp non-http(s) Location fix to reviewer spec
YunchuWang Jul 27, 2026
71e0205
Fix test-fixture typo (drop circular PR ref) and second u.port invali…
YunchuWang Jul 27, 2026
1d1d635
Correct sub-orch ID test comment: scheduler's executionId, not child's
YunchuWang Jul 27, 2026
e6f2861
Remove test-only credential-cache reset hook from production source
YunchuWang Jul 28, 2026
fde1b78
Guard callHttp Retry-After against Date overflow; correct v3-compat docs
YunchuWang Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 50 additions & 5 deletions packages/azure-functions-durable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,57 @@ changed:
`LockHandle` — call `release()`, ideally in a `finally`) and query with
`context.entities.isInCriticalSection()`. Restoring the v3 `df.lock` / `isLocked` surface is tracked
in [#317](https://github.com/microsoft/durabletask-js/issues/317).
- **`context.df.callHttp(...)` now throws.** v3 ran durable HTTP as a host-managed activity; the
consolidated gRPC backend has no equivalent primitive. Implement an HTTP activity in your app and
call it from the orchestrator. Restoring `callHttp` as a worker-side durable activity is tracked in
[#318](https://github.com/microsoft/durabletask-js/issues/318).
- **`context.df.callHttp(...)` is restored** as a worker-side durable HTTP call
([#318](https://github.com/microsoft/durabletask-js/issues/318)) — though **not** as a drop-in, fully
v3-equivalent replacement: the known incompatibilities and behavior differences listed below are
load-bearing for migration, so review them before relying on it. 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.
A behavior note and several deliberate hardening/compat differences from v3:
- **Cross-origin `202` poll credentials are stripped.** The `Location` returned with a `202` is
callee-controlled, so when it points to a **different origin** (scheme/host/port) the poll drops
`Authorization`, `Cookie`, and the `tokenSource` (no token is re-minted for the attacker), and the
`x-functions-key` header is **always** dropped (both same- and cross-origin). Same-origin polls
still forward headers and the `tokenSource`, so legitimate async patterns keep working. This
mirrors the .NET extension's policy
([Azure/azure-functions-durable-extension#3443](https://github.com/Azure/azure-functions-durable-extension/pull/3443)).
Comment thread
YunchuWang marked this conversation as resolved.
- **The initial request follows redirects with `fetch`'s defaults, which do not strip _custom_
credential headers.** Distinct from the `202` poll loop above, the first HTTP hop uses `fetch`'s
default `redirect: "follow"`. Per the Fetch Standard the implementation drops `Authorization` and
`Cookie` when a redirect crosses origins, but it does **not** drop custom credential headers such as
`x-functions-key`. Switching to `redirect: "manual"` with a per-hop cross-origin policy would close
this residual gap but change observable single-request semantics (hop count, effective URL, cookie
handling), so it is deliberately deferred; until then, avoid sending custom credential headers (e.g.
`x-functions-key`) to endpoints that may redirect cross-origin.
- **The built-in poll orchestrator cannot be started directly.** It is registered under a reserved
name (`BuiltIn__HttpPollOrchestrator`) and refuses a top-level start (it is only ever a
sub-orchestration of `callHttp`), so a dynamic `orchestrators/{name}` starter cannot be abused to
drive arbitrary SSRF or Managed-Identity token minting.
- **The default poll interval is 30 s** (matching the classic host) when a `202` carries no usable
`Retry-After`, rather than polling once per second.
- **Known incompatibility — a body on a `GET`/`HEAD` request throws.** v3 attached request content
regardless of method, and both the .NET extension (`TaskHttpActivityShim` builds the message with
no method check) and the durabletask-python SDK still pass the body to the request unconditionally.
The [Fetch Standard](https://fetch.spec.whatwg.org/) forbids a body on `GET`/`HEAD` and the
underlying `fetch` implementation rejects it, so this cannot be matched while `callHttp` is built on
`fetch`. Failing loudly was chosen over silently dropping the body (which would change the request
the app asked for): a migrated v3 workflow that relied on it must drop the `body` or switch to
`POST`/`PUT`/`PATCH`. Restoring the v3 behavior would require replacing `fetch` with a lower-level
HTTP transport, which is not planned.
- **Known incompatibility — `DurableHttpResponse` is a plain object, not a class.** The response
crosses the poll sub-orchestration's JSON boundary, so the v3 `response.getHeader(name)` method is
**not** available — existing `response.getHeader(...)` calls **fail at runtime** and must be
rewritten to index `response.headers[...]` by lower-cased key (response header names are lower-cased
by `fetch`).
- **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`
Comment thread
YunchuWang marked this conversation as resolved.
is re-exported from the core SDK (aggregate failures surface as JS-native `AggregateError`); use the
core `TestOrchestrationWorker` / `TestOrchestrationClient` for orchestration unit tests.
- **A plain non-generator classic orchestrator is no longer supported.** A classic v3 orchestrator
Expand Down
8 changes: 8 additions & 0 deletions packages/azure-functions-durable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@
"@grpc/grpc-js": "^1.14.4",
"@microsoft/durabletask-js": "0.4.0"
},
"peerDependencies": {
"@azure/identity": "^4.0.0"
},
"peerDependenciesMeta": {
"@azure/identity": {
"optional": true
}
},
"devDependencies": {
"@types/jest": "^29.5.1",
"@types/node": "^22.0.0",
Expand Down
15 changes: 15 additions & 0 deletions packages/azure-functions-durable/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import * as trigger from "./trigger";
import { ClassicEntity, wrapEntity } from "./entity-context";
import { ClassicOrchestrator, wrapOrchestrator } from "./orchestration-context";
import { DurableFunctionsWorker } from "./worker";
import {
BUILTIN_HTTP_ACTIVITY_NAME,
BUILTIN_HTTP_POLL_ORCHESTRATOR_NAME,
builtinHttpActivity,
builtinHttpPollOrchestrator,
} from "./http/builtin";

// The `client` namespace groups the durable-client starter registration helpers so that
// `df.app.client.http(...)` (and siblings) restore the v3 client-starter sugar. `index.ts`
Expand Down Expand Up @@ -157,3 +163,12 @@ function extractBase64Request(triggerInput: unknown): string {
}
throw new TypeError("Durable trigger did not provide a base64-encoded request body.");
}

// Auto-register the built-in durable-HTTP functions exactly once when this module is first imported,
// so classic `context.df.callHttp` works with no user registration. The poll orchestrator is a
// core-native `async function*` (passed through by `wrapOrchestrator`) and the activity is a plain
// host-dispatched handler. Names are reserved (see `./http/builtin`); registering here — rather than
// per app instance — means they are wired once for the whole function app. Ported from the
// durabletask-python design (Andy Staples, durabletask-python#155).
orchestration(BUILTIN_HTTP_POLL_ORCHESTRATOR_NAME, { handler: builtinHttpPollOrchestrator });
activity(BUILTIN_HTTP_ACTIVITY_NAME, { handler: builtinHttpActivity });
Loading
Loading