Skip to content

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:

  1. Load (indexing). The host sends a FunctionLoadRequest describing one function and its bindings. The worker inspects your Python function's signature and type hints to reconcile declared parameters with the binding metadata.
  2. Invocation. For each request the host sends an InvocationRequest carrying a list of ParameterBinding entries. 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 to def), 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 loadazure-functions-python-worker functions.py L383-L387, tag azure_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 invokedispatcher.py L596-L688, tag azure_functions_worker-4.45.1. link
  • FunctionLoadRequest and ParameterBinding protobuf shapesFunctionRpc.proto L281-L523, commit 0f52efccc6e19a7f108cc36d6d43f33866d28e52. link
  • FunctionBuilder.build() — where the handler is validated and registeredazure-functions-python-library function_app.py L226-L234, tag 1.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: