How the worker binds handlers¶
When you write a Python v2 function, you attach decorators to a plain function
and the Azure Functions worker does the rest: it discovers your function,
learns its parameters, and — on every request — hands your function the right
arguments by name. Several toolkit packages (validation, db,
knowledge, and the doctor decorator-order rule) depend on exactly how that
binding works. This page documents that contract once.
Everything here is scoped to the observable platform contract — the
FunctionRpc messages and the worker's load/invocation behavior — with each
claim pinned to upstream source. It is not a tour of worker internals.
The two phases¶
The worker interacts with your handler in two distinct phases:
- Load (indexing). The host sends a
FunctionLoadRequestdescribing one function and its bindings. The worker inspects your Python function's signature and type hints to reconcile declared parameters with the binding metadata. - Invocation. For each request the host sends an
InvocationRequestcarrying a list ofParameterBindingentries. The worker builds an argument dict keyed by binding name, injects the invocation context when your function asks for it, and calls your function.
Host ──FunctionLoadRequest──▶ Worker: inspect.signature + get_type_hints (load)
Host ──InvocationRequest────▶ Worker: args[pb.name] = ...; call handler (invoke)
Binding¶
At load time the worker reads your function's parameters directly from the
Python object using inspect.signature() and typing.get_type_hints()
(functions.py#L383-L387):
func_name = metadata.name
sig = inspect.signature(func)
params = dict(sig.parameters)
annotations = typing.get_type_hints(func)
At invocation time, each incoming ParameterBinding is matched to a parameter
by its name, not by position (dispatcher.py#L596-L688):
for pb in invoc_request.input_data:
pb_type_info = fi.input_types[pb.name]
args[pb.name] = bindings.from_incoming_proto(pb_type_info.binding_name, pb, ...)
The name here is the same name field defined on the ParameterBinding
protobuf message (FunctionRpc.proto#L281-L523):
message ParameterBinding {
string name = 1;
oneof rpc_data {
TypedData data = 2;
RpcSharedMemory rpc_shared_memory = 3;
}
}
Consequence — parameter names are part of your contract. Because binding is
by name, a decorator that renames, drops, or fails to preserve a parameter (for
example by wrapping without functools.wraps, or by replacing the function with
an object the worker can no longer introspect) breaks binding. This is why the
toolkit's decorators are careful to keep the wrapped function's __signature__
and __annotations__ intact.
Invocation context and invocation_id¶
The same invocation path sets the current invocation_id on the running task
(so your logs correlate) and injects the invocation context into your
handler only when the function declares it (dispatcher.py#L596-L688):
invocation_id = invoc_request.invocation_id
current_task.set_azure_invocation_id(invocation_id)
...
if fi.requires_context:
args['context'] = fi_context
So context is just another name-bound argument: declare a context
parameter and the worker fills it; omit it and the worker does not. Nothing is
injected positionally.
Why decorator order matters¶
In the v2 programming model your decorators build up a
FunctionBuilder. The azure-functions library's
FunctionBuilder.build() validates the configured function and returns the
Function object the worker indexes (function_app.py#L226-L234):
def build(self, auth_level: Optional[AuthLevel] = None) -> Function:
self._validate_function(auth_level)
return self._function
A trigger decorator such as @app.route expects to wrap your handler. If a
metadata-attaching decorator (e.g. @validate_http) is stacked outside the
trigger, it receives a FunctionBuilder instead of the handler — the wrong
object — and the metadata it tries to attach is lost or the build raises. The
rule that falls out of the platform contract:
Put Azure trigger decorators outermost (closest to
@app), and toolkit/metadata decorators innermost (closest todef), so each layer wraps the object it expects.
References¶
All links are pinned to a release tag or commit SHA so they will not drift.
- Signature & type-hint inspection at load —
azure-functions-python-workerfunctions.pyL383-L387, tagazure_functions_worker-4.45.1: the worker reads parameter names and annotations from your function. link - Name-keyed argument binding +
invocation_id/context injection at invoke —dispatcher.pyL596-L688, tagazure_functions_worker-4.45.1. link FunctionLoadRequestandParameterBindingprotobuf shapes —FunctionRpc.protoL281-L523, commit0f52efccc6e19a7f108cc36d6d43f33866d28e52. linkFunctionBuilder.build()— where the handler is validated and registered —azure-functions-python-libraryfunction_app.pyL226-L234, tag1.17.0. link
Version-skew surface¶
Everything above is worker 1.x / 4.45.x observable behavior. Azure hosts the
worker and rolls it forward independently, so a future major worker could change
how signatures are introspected or how bindings are keyed. The toolkit's defense
is twofold: the validation package pins azure-functions<2.0.0
(2.0 readiness notes)
and runs a weekly
worker-nightly lane
that exercises the WorkerCompat suite against the latest pre-release
azure-functions package, so signature-override or name-binding drift surfaces
before it reaches a deployment.
Where this contract is enforced in the toolkit¶
Each claim above is locked by an existing test, not just asserted here:
azure-functions-validationraises at decoration time when a metadata decorator receives aFunctionBuilder, and preserves__signature__/__annotations__so name-binding keeps working. Locked bytests/test_decorator.pyL167-241,333-500 (signature/annotation preservation and builder-received guard) andtests/test_worker_compat_2x_spike.pyL63-92 (worker-visible passthrough signature).azure-functions-doctorships acheck_decorator_orderrule whose hint links back to this page.