Service
Blob Storage (account resolution / request routing) — impact lands on Azure Functions host AzureWebJobsStorage
Azure API Action
Create Container (PUT /{container}?restype=container), and every blob operation addressed through a host-style endpoint.
Expected behavior
Real Azure addresses storage host-style: the account is in the hostname and the container is the first path segment.
PUT https://devstoreaccount1.blob.core.windows.net/azure-webjobs-hosts?restype=container
AzureRoutingFilter should resolve accountName from the Host header when the host carries one, and only fall back to the first path segment otherwise. That would make both addressing styles work:
| Style |
URL |
account |
container |
| host-style (real Azure) |
Host: devstoreaccount1.… + /azure-webjobs-hosts |
devstoreaccount1 |
azure-webjobs-hosts |
| path-style (floci today) |
/devstoreaccount1/azure-webjobs-hosts |
devstoreaccount1 |
azure-webjobs-hosts |
Actual behavior
The account is resolved only from the first path segment; the Host header is ignored. A host-style request therefore has its container name consumed as the account, leaving no container, and Create Container answers 501 NotImplemented:
$ curl -X PUT 'http://localhost:4577/awjh2?restype=container' \
-H 'Host: devstoreaccount1.blob.localhost:4577' -H 'x-ms-version: 2024-11-04'
HTTP/1.1 501 Not Implemented
<Error><Code>NotImplemented</Code><Message>The requested operation is not implemented.</Message></Error>
# floci log:
INFO [io.flo.az.cor.AzureRoutingFilter] Resolved accountName: awjh2, serviceType: blob, resourcePath:
accountName: awjh2 — the container name was taken as the account.
Why this blocks Durable/timer Functions (the #136 / #183 scenario)
With the connection string the README documents for path-style access:
DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8…;
BlobEndpoint=http://floci-az:4577/devstoreaccount1;
QueueEndpoint=http://floci-az:4577/devstoreaccount1-queue;
TableEndpoint=http://floci-az:4577/devstoreaccount1-table;
the .NET Azure Functions host (Microsoft.Azure.WebJobs.Host.BlobLeaseDistributedLockManager) emits requests with no container segment at all — the container azure-webjobs-hosts never appears:
PUT /devstoreaccount1?restype=container -> 501 # create azure-webjobs-hosts
HEAD /devstoreaccount1/locks/<hostid>/host -> 404
PUT /devstoreaccount1/locks/<hostid>/host -> 404
PUT /devstoreaccount1/locks/<hostid>/host?comp=lease -> 404
GET /devstoreaccount1/?comp=list&maxresults=1 -> 200 # reachability probe passes
Azure.Storage.Blobs appears to treat a BlobEndpoint that already carries a path as already pointing at a container, so the requested container name is discarded rather than appended. (That inference is from the wire traffic above — I haven't traced the SDK source, so the exact mechanism may differ; the observable behavior is reproducible.) Whatever the cause, the SDK produces host-style-shaped requests that floci cannot route, and there is no connection-string shape that satisfies both sides today:
- path-style (
BlobEndpoint=.../devstoreaccount1) — floci routes it, .NET drops the container name.
- host-style (
BlobEndpoint=http://devstoreaccount1.floci-az:4577) — .NET builds it correctly, floci reads the container as the account → 501.
UseDevelopmentStorage=true;DevelopmentStorageProxyUri=… — the emulator mode .NET handles natively, but it requires ports 10000/10001/10002 with per-service paths that floci (single port 4577, -queue/-table path suffixes) doesn't serve.
The 200 on the reachability probe makes this notably hard to diagnose: the host concludes storage is healthy, then fails in the listener-start retry loop with NotImplemented rather than reporting a storage-configuration problem.
Net effect: the Table error-envelope fix (#182) and blob-lease support (#183) both work correctly on 0.12.0 — I verified each directly — but Functions still can't use floci-az as AzureWebJobsStorage, because the lock blobs are never addressed to the right container.
Verification that this is the only remaining blocker
Both 0.12.0 fixes confirmed working against floci/floci-az:0.12.0:
# #182 — Table OData error envelope
POST /devstoreaccount1-table/Tables {"TableName":"probeTbl"} -> 201
POST (again) -> 409
content-type: application/json;odata=minimalmetadata
{"odata.error":{"code":"TableAlreadyExists","message":{"value":"…","lang":"en-US"}}}
# #183 — blob leases
PUT /devstoreaccount1/probe-locks/lock1?comp=lease (acquire, 15s) -> 201 x-ms-lease-id: aae3afe3-…
PUT /devstoreaccount1/probe-locks/lock1 (no lease id) -> 412 <Code>LeaseIdMissing</Code>
PUT /devstoreaccount1/probe-locks/lock1 (with lease id) -> 201
GET /devstoreaccount1/probe-locks/lock1 -> x-ms-lease-state: leased / status: locked
Control: the identical Azure Functions container, same image and same app code, pointed at Azurite via UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://azurite starts its listener and runs its timer:
Executed 'Functions.azf-timer-run' (Succeeded, Duration=77ms)
Pointed at floci-az, the same host loops on The listener for function 'Functions.azf-timer-run' was unable to start / NotImplemented.
Reproduction
docker run -d -p 4577:4577 floci/floci-az:0.12.0
- Run an Azure Functions container (
mcr.microsoft.com/azure-functions/python:4-python3.12) holding any timer-triggered function, with AzureWebJobsStorage set to the path-style connection string above.
- The listener never starts. Set
QUARKUS_HTTP_ACCESS_LOG_ENABLED=true on floci to see PUT /devstoreaccount1?restype=container -> 501.
A timer trigger is sufficient — it takes a WebJobs singleton lease through the same code path Durable uses for partition management, so it reproduces this in seconds without a durable orchestration.
Environment
- floci-az
0.12.0 (floci/floci-az:0.12.0, native linux/arm64)
- Azure Functions host image
mcr.microsoft.com/azure-functions/python:4-python3.12
- macOS 15 / Apple Silicon, Docker via colima
- Reference: Azurite 3.35.0 (working control)
Are you willing to contribute a PR?
Happy to implement the Host-header account resolution in AzureRoutingFilter (preferring the header, falling back to path segment 1 so every existing path-style caller is unaffected) if that's the direction you'd like — same approach as #182/#183.
Service
Blob Storage (account resolution / request routing) — impact lands on Azure Functions host
AzureWebJobsStorageAzure API Action
Create Container (
PUT /{container}?restype=container), and every blob operation addressed through a host-style endpoint.Expected behavior
Real Azure addresses storage host-style: the account is in the hostname and the container is the first path segment.
AzureRoutingFiltershould resolveaccountNamefrom theHostheader when the host carries one, and only fall back to the first path segment otherwise. That would make both addressing styles work:Host: devstoreaccount1.…+/azure-webjobs-hostsdevstoreaccount1azure-webjobs-hosts/devstoreaccount1/azure-webjobs-hostsdevstoreaccount1azure-webjobs-hostsActual behavior
The account is resolved only from the first path segment; the
Hostheader is ignored. A host-style request therefore has its container name consumed as the account, leaving no container, and Create Container answers501 NotImplemented:accountName: awjh2— the container name was taken as the account.Why this blocks Durable/timer Functions (the #136 / #183 scenario)
With the connection string the README documents for path-style access:
the .NET Azure Functions host (
Microsoft.Azure.WebJobs.Host.BlobLeaseDistributedLockManager) emits requests with no container segment at all — the containerazure-webjobs-hostsnever appears:Azure.Storage.Blobsappears to treat aBlobEndpointthat already carries a path as already pointing at a container, so the requested container name is discarded rather than appended. (That inference is from the wire traffic above — I haven't traced the SDK source, so the exact mechanism may differ; the observable behavior is reproducible.) Whatever the cause, the SDK produces host-style-shaped requests that floci cannot route, and there is no connection-string shape that satisfies both sides today:BlobEndpoint=.../devstoreaccount1) — floci routes it, .NET drops the container name.BlobEndpoint=http://devstoreaccount1.floci-az:4577) — .NET builds it correctly, floci reads the container as the account → 501.UseDevelopmentStorage=true;DevelopmentStorageProxyUri=…— the emulator mode .NET handles natively, but it requires ports 10000/10001/10002 with per-service paths that floci (single port 4577,-queue/-tablepath suffixes) doesn't serve.The
200on the reachability probe makes this notably hard to diagnose: the host concludes storage is healthy, then fails in the listener-start retry loop withNotImplementedrather than reporting a storage-configuration problem.Net effect: the Table error-envelope fix (#182) and blob-lease support (#183) both work correctly on 0.12.0 — I verified each directly — but Functions still can't use floci-az as
AzureWebJobsStorage, because the lock blobs are never addressed to the right container.Verification that this is the only remaining blocker
Both 0.12.0 fixes confirmed working against
floci/floci-az:0.12.0:Control: the identical Azure Functions container, same image and same app code, pointed at Azurite via
UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://azuritestarts its listener and runs its timer:Pointed at floci-az, the same host loops on
The listener for function 'Functions.azf-timer-run' was unable to start/NotImplemented.Reproduction
docker run -d -p 4577:4577 floci/floci-az:0.12.0mcr.microsoft.com/azure-functions/python:4-python3.12) holding any timer-triggered function, withAzureWebJobsStorageset to the path-style connection string above.QUARKUS_HTTP_ACCESS_LOG_ENABLED=trueon floci to seePUT /devstoreaccount1?restype=container -> 501.A timer trigger is sufficient — it takes a WebJobs singleton lease through the same code path Durable uses for partition management, so it reproduces this in seconds without a durable orchestration.
Environment
0.12.0(floci/floci-az:0.12.0, nativelinux/arm64)mcr.microsoft.com/azure-functions/python:4-python3.12Are you willing to contribute a PR?
Happy to implement the
Host-header account resolution inAzureRoutingFilter(preferring the header, falling back to path segment 1 so every existing path-style caller is unaffected) if that's the direction you'd like — same approach as #182/#183.