hashicorp/terraform · error

failed to send completed event: %w

Error message

failed to send completed event: %w

What it means

Returned by InvokeAction (internal/grpcwrap/provider6.go:1261) when server.Send fails while emitting the Completed event over the InvokeAction server-streaming RPC. The handler iterates provider InvokeAction events; on a Completed event it converts diagnostics to proto and calls server.Send, and if that returns a non-nil error it is wrapped with %w and returned. The cause is a broken/cancelled downstream gRPC stream.

Source

Thrown at internal/grpcwrap/provider6.go:1261

			server.Send(&tfplugin6.InvokeAction_Event{
				Type: &tfplugin6.InvokeAction_Event_Progress_{
					Progress: &tfplugin6.InvokeAction_Event_Progress{
						Message: invokeEvt.Message,
					},
				},
			})

		case providers.InvokeActionEvent_Completed:
			completed := &tfplugin6.InvokeAction_Event_Completed{}
			completed.Diagnostics = convert.AppendProtoDiag(completed.Diagnostics, invokeEvt.Diagnostics)

			err := server.Send(&tfplugin6.InvokeAction_Event{
				Type: &tfplugin6.InvokeAction_Event_Completed_{
					Completed: completed,
				},
			})
			if err != nil {
				return fmt.Errorf("failed to send completed event: %w", err)
			}
		}

	}

	return nil
}

func (p *provider6) ValidateActionConfig(_ context.Context, req *tfplugin6.ValidateActionConfig_Request) (*tfplugin6.ValidateActionConfig_Response, error) {
	resp := &tfplugin6.ValidateActionConfig_Response{}
	ty := p.schema.Actions[req.TypeName].ConfigSchema.ImpliedType()

	configVal, err := decodeDynamicValue6(req.Config, ty)
	if err != nil {
		resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
		return resp, nil
	}

View on GitHub (pinned to c9def3e214)

Solutions

  1. Distinguish context cancellation (errors.Is(err, context.Canceled)) from real transport failures.
  2. For long actions, ensure gRPC keepalive/deadline are generous enough to reach the Completed send.
  3. Retry the action invocation if idempotent, after verifying the client is still connected.
  4. If authoring a client, always drain the InvokeAction stream to completion before closing.

Example fix

// before
err := server.Send(&tfplugin6.InvokeAction_Event{
    Type: &tfplugin6.InvokeAction_Event_Completed_{Completed: completed},
})  // client already gone -> failed to send completed event: ...

// after (client side: always receive until EOF)
for {
    ev, err := stream.Recv()
    if err == io.EOF { break }
    if err != nil { return err }
    handle(ev)
}
Defensive patterns

Strategy: retry

Validate before calling

// Before sending the Completed event, confirm the stream context is live:
func streamReady(ctx context.Context) bool { return ctx.Err() == nil }

Type guard

func isClientGone(err error) bool {
    return errors.Is(err, context.Canceled) || status.Code(err) == codes.Unavailable
}

Try / catch

// Clients should drain the stream; if send fails on a transient error, retry the action:
for attempt := 0; attempt < 3; attempt++ {
    err := runInvokeAction(ctx, req)
    if err == nil || !isClientGone(err) { return err }
}

Prevention

When it happens

Trigger: The gRPC client (Terraform core) has disconnected, cancelled the context, or the stream broke exactly when the provider finished and tried to send its terminal Completed event; server.Send returns the transport error.

Common situations: User cancels (Ctrl-C) right as an action completes, context deadline exceeded for long-running actions, network drop, or the client process exiting before draining the stream.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/920d9d9c770a8e52. Report an issue: GitHub.