From 002627f57f51680f18ba6ec856e400e3744e746d Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 26 Feb 2026 22:04:02 -0500 Subject: [PATCH 1/2] fix: improve polling patterns across deployment services - Stack deployments: replace constant 5s polling with exponential backoff (1s initial, 10s cap) for both subscription and resource group scoped deployment stack lookups - FuncApp host: fix bug where Retry-After header value was computed but ignored (delay was reassigned to pollDelay instead of retryAfter). Also make 404-retry sleep context-aware. - Static Web App: make deployment verification sleep context-aware so Ctrl+C responds promptly instead of blocking for up to 5s. Addresses findings 10, 11, 12 from Azure/azure-dev#6886 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/azapi/stack_deployments.go | 10 ++++++++-- cli/azd/pkg/azsdk/funcapp_host_client.go | 8 ++++++-- cli/azd/pkg/project/service_target_staticwebapp.go | 6 +++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/cli/azd/pkg/azapi/stack_deployments.go b/cli/azd/pkg/azapi/stack_deployments.go index f410d83a255..c9197d6b2ac 100644 --- a/cli/azd/pkg/azapi/stack_deployments.go +++ b/cli/azd/pkg/azapi/stack_deployments.go @@ -125,7 +125,10 @@ func (d *StackDeployments) GetSubscriptionDeployment( err = retry.Do( ctx, - retry.WithMaxDuration(10*time.Minute, retry.NewConstant(5*time.Second)), + retry.WithMaxDuration( + 10*time.Minute, + retry.WithCappedDuration(10*time.Second, retry.NewExponential(1*time.Second)), + ), func(ctx context.Context) error { response, err := client.GetAtSubscription(ctx, deploymentName, nil) if err != nil { @@ -201,7 +204,10 @@ func (d *StackDeployments) GetResourceGroupDeployment( err = retry.Do( ctx, - retry.WithMaxDuration(10*time.Minute, retry.NewConstant(5*time.Second)), + retry.WithMaxDuration( + 10*time.Minute, + retry.WithCappedDuration(10*time.Second, retry.NewExponential(1*time.Second)), + ), func(ctx context.Context) error { response, err := client.GetAtResourceGroup(ctx, resourceGroupName, deploymentName, nil) if err != nil { diff --git a/cli/azd/pkg/azsdk/funcapp_host_client.go b/cli/azd/pkg/azsdk/funcapp_host_client.go index d7b3431747d..8807cf14e65 100644 --- a/cli/azd/pkg/azsdk/funcapp_host_client.go +++ b/cli/azd/pkg/azsdk/funcapp_host_client.go @@ -139,7 +139,11 @@ func (c *FuncAppHostClient) waitForDeployment(ctx context.Context, location stri if deploymentNotFoundAttempts <= 3 && response.StatusCode == http.StatusNotFound { deploymentNotFoundAttempts++ - time.Sleep(pollDelay) + select { + case <-ctx.Done(): + return PublishResponse{}, ctx.Err() + case <-time.After(pollDelay): + } continue } @@ -181,7 +185,7 @@ func (c *FuncAppHostClient) waitForDeployment(ctx context.Context, location stri delay := pollDelay if retryAfter := httputil.RetryAfter(response); retryAfter > 0 { - delay = pollDelay + delay = retryAfter } select { diff --git a/cli/azd/pkg/project/service_target_staticwebapp.go b/cli/azd/pkg/project/service_target_staticwebapp.go index ec3167cdd44..291f3124a42 100644 --- a/cli/azd/pkg/project/service_target_staticwebapp.go +++ b/cli/azd/pkg/project/service_target_staticwebapp.go @@ -290,7 +290,11 @@ func (at *staticWebAppTarget) verifyDeployment(ctx context.Context, targetResour return fmt.Errorf("failed verifying static web app deployment. Still in %s state", envProps.Status) } - time.Sleep(5 * time.Second) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(5 * time.Second): + } } return nil From 1881552815e253795e862dc31d5e4813c5a5518f Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 27 Feb 2026 15:45:06 -0500 Subject: [PATCH 2/2] fix: close response.Body in waitForDeployment polling loop Close response.Body on every iteration of the polling loop to prevent HTTP connection leaks. Previously the body was never closed, especially on 404 retry paths where the body was not consumed at all. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/azsdk/funcapp_host_client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/azsdk/funcapp_host_client.go b/cli/azd/pkg/azsdk/funcapp_host_client.go index 8807cf14e65..89622229b74 100644 --- a/cli/azd/pkg/azsdk/funcapp_host_client.go +++ b/cli/azd/pkg/azsdk/funcapp_host_client.go @@ -138,6 +138,7 @@ func (c *FuncAppHostClient) waitForDeployment(ctx context.Context, location stri } if deploymentNotFoundAttempts <= 3 && response.StatusCode == http.StatusNotFound { + response.Body.Close() deploymentNotFoundAttempts++ select { case <-ctx.Done(): @@ -153,19 +154,24 @@ func (c *FuncAppHostClient) waitForDeployment(ctx context.Context, location stri // This 404 is due to the deployment worker being "recycled". // This shortcoming would be fixed by work item https://msazure.visualstudio.com/Antares/_workitems/edit/24715654. if response.StatusCode == http.StatusNotFound && lastResponse != nil { + response.Body.Close() return *lastResponse, nil } if response.StatusCode != http.StatusAccepted && response.StatusCode != http.StatusOK { - return PublishResponse{}, runtime.NewResponseError(response) + err := runtime.NewResponseError(response) + response.Body.Close() + return PublishResponse{}, err } // Server always returns status code of OK-200 whether the deployment is in-progress or complete. // Thus, we always read the response body to determine the actual status. resp := PublishResponse{} if err := runtime.UnmarshalAsJSON(response, &resp); err != nil { + response.Body.Close() return PublishResponse{}, err } + response.Body.Close() switch resp.Status { case PublishStatusCancelled: