Merge "cmd/principal: Usability tweaks."
diff --git a/services/device/device/devicemanager_mock_test.go b/services/device/device/devicemanager_mock_test.go
index 2d34144..829f77f 100644
--- a/services/device/device/devicemanager_mock_test.go
+++ b/services/device/device/devicemanager_mock_test.go
@@ -205,7 +205,9 @@
 	return mdi.simpleCore("Run", "Run")
 }
 
-func (i *mockDeviceInvoker) Revert(*context.T, rpc.ServerCall) error { return nil }
+func (mdi *mockDeviceInvoker) Revert(*context.T, rpc.ServerCall) error {
+	return mdi.simpleCore("Revert", "Revert")
+}
 
 type InstantiateResponse struct {
 	err        error
diff --git a/services/device/device/doc.go b/services/device/device/doc.go
index 881e621..54a832c 100644
--- a/services/device/device/doc.go
+++ b/services/device/device/doc.go
@@ -23,8 +23,8 @@
    delete        Delete the given application instance.
    run           Run the given application instance.
    kill          Kill the given application instance.
-   revert        Revert the device manager or application
-   update        Update device manager or applications.
+   revert        Revert the device manager or applications.
+   update        Update the device manager or applications.
    status        Get device manager or application status.
    debug         Debug the device.
    acl           Tool for setting device manager Permissions
@@ -283,24 +283,24 @@
 
 Device revert
 
-Revert the device manager or application to its previous version
+Revert the device manager or application instances and installations to a
+previous version of their current version
 
 Usage:
-   device revert <object>
+   device revert <name patterns...>
 
-<object> is the vanadium object name of the device manager or application
-installation to revert.
+<name patterns...> are vanadium object names or glob name patterns corresponding
+to the device manager service, or to application installations and instances.
 
 Device update
 
 Update the device manager or application instances and installations
 
 Usage:
-   device update <app name patterns...>
+   device update <name patterns...>
 
-<app name patterns...> are vanadium object names or glob name patterns
-corresponding to the device manager service, or to application installations and
-instances.
+<name patterns...> are vanadium object names or glob name patterns corresponding
+to the device manager service, or to application installations and instances.
 
 Device status
 
diff --git a/services/device/device/revert.go b/services/device/device/revert.go
deleted file mode 100644
index 0f27078..0000000
--- a/services/device/device/revert.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package main
-
-import (
-	"fmt"
-
-	"v.io/v23/context"
-	"v.io/v23/services/device"
-	"v.io/x/lib/cmdline"
-	"v.io/x/ref/lib/v23cmd"
-)
-
-var cmdRevert = &cmdline.Command{
-	Runner:   v23cmd.RunnerFunc(runRevert),
-	Name:     "revert",
-	Short:    "Revert the device manager or application",
-	Long:     "Revert the device manager or application to its previous version",
-	ArgsName: "<object>",
-	ArgsLong: `
-<object> is the vanadium object name of the device manager or application
-installation to revert.`,
-}
-
-func runRevert(ctx *context.T, env *cmdline.Env, args []string) error {
-	if expected, got := 1, len(args); expected != got {
-		return env.UsageErrorf("revert: incorrect number of arguments, expected %d, got %d", expected, got)
-	}
-	deviceName := args[0]
-	if err := device.ApplicationClient(deviceName).Revert(ctx); err != nil {
-		return err
-	}
-	fmt.Fprintln(env.Stdout, "Revert successful.")
-	return nil
-}
diff --git a/services/device/device/update.go b/services/device/device/update.go
index e6cb007..e2d1f9a 100644
--- a/services/device/device/update.go
+++ b/services/device/device/update.go
@@ -4,6 +4,8 @@
 
 package main
 
+// TODO(caprita): Rename to update_revert.go
+
 import (
 	"fmt"
 	"io"
@@ -20,11 +22,21 @@
 var cmdUpdate = &cmdline.Command{
 	Runner:   globRunner(runUpdate),
 	Name:     "update",
-	Short:    "Update device manager or applications.",
+	Short:    "Update the device manager or applications.",
 	Long:     "Update the device manager or application instances and installations",
-	ArgsName: "<app name patterns...>",
+	ArgsName: "<name patterns...>",
 	ArgsLong: `
-<app name patterns...> are vanadium object names or glob name patterns corresponding to the device manager service, or to application installations and instances.`,
+<name patterns...> are vanadium object names or glob name patterns corresponding to the device manager service, or to application installations and instances.`,
+}
+
+var cmdRevert = &cmdline.Command{
+	Runner:   globRunner(runRevert),
+	Name:     "revert",
+	Short:    "Revert the device manager or applications.",
+	Long:     "Revert the device manager or application instances and installations to a previous version of their current version",
+	ArgsName: "<name patterns...>",
+	ArgsLong: `
+<name patterns...> are vanadium object names or glob name patterns corresponding to the device manager service, or to application installations and instances.`,
 }
 
 func instanceIsRunning(ctx *context.T, von string) (bool, error) {
@@ -39,7 +51,11 @@
 	return s.Value.State == device.InstanceStateRunning, nil
 }
 
-func updateInstance(ctx *context.T, stdout, stderr io.Writer, name string, status device.StatusInstance) (retErr error) {
+var revertOrUpdate = map[bool]string{true: "revert", false: "update"}
+var revertOrUpdateMethod = map[bool]string{true: "Revert", false: "Update"}
+var revertOrUpdateNoOp = map[bool]string{true: "no previous version available", false: "already up to date"}
+
+func changeVersionInstance(ctx *context.T, stdout, stderr io.Writer, name string, status device.StatusInstance, revert bool) (retErr error) {
 	if status.Value.State == device.InstanceStateRunning {
 		if err := device.ApplicationClient(name).Kill(ctx, killDeadline); err != nil {
 			// Check the app's state again in case we killed it,
@@ -54,10 +70,10 @@
 			if running {
 				return fmt.Errorf("Kill failed: %v", err)
 			}
-			fmt.Fprintf(stderr, "WARNING for \"%s\": recovered from Kill error (%s). Proceeding with update.\n", name, err)
+			fmt.Fprintf(stderr, "WARNING for \"%s\": recovered from Kill error (%s). Proceeding with %s.\n", name, err, revertOrUpdate[revert])
 		}
 		// App was running, and we killed it, so we need to run it again
-		// after the update.
+		// after the update/revert.
 		defer func() {
 			if err := device.ApplicationClient(name).Run(ctx); err != nil {
 				err = fmt.Errorf("Run failed: %v", err)
@@ -69,43 +85,63 @@
 			}
 		}()
 	}
-	// Update the instance.
-	switch err := device.ApplicationClient(name).Update(ctx); {
+	// Update/revert the instance.
+	var err error
+	if revert {
+		err = device.ApplicationClient(name).Revert(ctx)
+	} else {
+		err = device.ApplicationClient(name).Update(ctx)
+	}
+	switch {
 	case err == nil:
-		fmt.Fprintf(stdout, "Successfully updated instance \"%s\".\n", name)
+		fmt.Fprintf(stdout, "Successful %s of version for instance \"%s\".\n", revertOrUpdate[revert], name)
 		return nil
 	case verror.ErrorID(err) == deviceimpl.ErrUpdateNoOp.ID:
 		// TODO(caprita): Ideally, we wouldn't even attempt a kill /
-		// restart if there's no newer version of the application.
-		fmt.Fprintf(stdout, "Instance \"%s\" already up to date.\n", name)
+		// restart if the update/revert is a no-op.
+		fmt.Fprintf(stdout, "Instance \"%s\": %s.\n", name, revertOrUpdateNoOp[revert])
 		return nil
 	default:
-		return fmt.Errorf("Update failed: %v", err)
+		return fmt.Errorf("%s failed: %v", revertOrUpdateMethod[revert], err)
 	}
 }
 
-func updateOne(ctx *context.T, what string, stdout, stderr io.Writer, name string) error {
-	switch err := device.ApplicationClient(name).Update(ctx); {
+func changeVersionOne(ctx *context.T, what string, stdout, stderr io.Writer, name string, revert bool) error {
+	var err error
+	if revert {
+		err = device.ApplicationClient(name).Revert(ctx)
+	} else {
+		err = device.ApplicationClient(name).Update(ctx)
+	}
+	switch {
 	case err == nil:
-		fmt.Fprintf(stdout, "Successfully updated version for %s \"%s\".\n", what, name)
+		fmt.Fprintf(stdout, "Successful %s of version for %s \"%s\".\n", revertOrUpdate[revert], what, name)
 		return nil
 	case verror.ErrorID(err) == deviceimpl.ErrUpdateNoOp.ID:
-		fmt.Fprintf(stdout, "%s \"%s\" already up to date.\n", what, name)
+		fmt.Fprintf(stdout, "%s \"%s\": %s.\n", what, name, revertOrUpdateNoOp[revert])
 		return nil
 	default:
-		return fmt.Errorf("Update failed: %v", err)
+		return fmt.Errorf("%s failed: %v", revertOrUpdateMethod[revert], err)
+	}
+}
+
+func changeVersion(entry globResult, ctx *context.T, stdout, stderr io.Writer, revert bool) error {
+	switch entry.kind {
+	case applicationInstanceObject:
+		return changeVersionInstance(ctx, stdout, stderr, entry.name, entry.status.(device.StatusInstance), revert)
+	case applicationInstallationObject:
+		return changeVersionOne(ctx, "installation", stdout, stderr, entry.name, revert)
+	case deviceServiceObject:
+		return changeVersionOne(ctx, "device service", stdout, stderr, entry.name, revert)
+	default:
+		return fmt.Errorf("unhandled object kind %v", entry.kind)
 	}
 }
 
 func runUpdate(entry globResult, ctx *context.T, stdout, stderr io.Writer) error {
-	switch entry.kind {
-	case applicationInstanceObject:
-		return updateInstance(ctx, stdout, stderr, entry.name, entry.status.(device.StatusInstance))
-	case applicationInstallationObject:
-		return updateOne(ctx, "installation", stdout, stderr, entry.name)
-	case deviceServiceObject:
-		return updateOne(ctx, "device service", stdout, stderr, entry.name)
-	default:
-		return fmt.Errorf("unhandled object kind %v", entry.kind)
-	}
+	return changeVersion(entry, ctx, stdout, stderr, false)
+}
+
+func runRevert(entry globResult, ctx *context.T, stdout, stderr io.Writer) error {
+	return changeVersion(entry, ctx, stdout, stderr, true)
 }
diff --git a/services/device/device/update_test.go b/services/device/device/update_test.go
index 1c90110..a6f0a40 100644
--- a/services/device/device/update_test.go
+++ b/services/device/device/update_test.go
@@ -4,6 +4,8 @@
 
 package main_test
 
+// TODO(caprita): Rename to update_revert_test.go
+
 import (
 	"bytes"
 	"fmt"
@@ -11,6 +13,8 @@
 	"strings"
 	"testing"
 	"time"
+	"unicode"
+	"unicode/utf8"
 
 	"v.io/v23/naming"
 	"v.io/x/lib/cmdline"
@@ -20,8 +24,16 @@
 	cmd_device "v.io/x/ref/services/device/device"
 )
 
-// TestUpdateCommand verifies the device update command.
-func TestUpdateCommand(t *testing.T) {
+func capitalize(s string) string {
+	r, size := utf8.DecodeRuneInString(s)
+	if r == utf8.RuneError {
+		return ""
+	}
+	return string(unicode.ToUpper(r)) + s[size:]
+}
+
+// TestUpdateAndRevertCommands verifies the device update and revert commands.
+func TestUpdateAndRevertCommands(t *testing.T) {
 	ctx, shutdown := test.V23Init()
 	defer shutdown()
 	tapes := newTapeMap()
@@ -31,7 +43,7 @@
 	}
 	defer stopServer(t, server)
 
-	cmd := cmd_device.CmdRoot
+	root := cmd_device.CmdRoot
 	appName := naming.JoinAddressName(endpoint.String(), "app")
 	rootTape := tapes.forSuffix("")
 	globName := naming.JoinAddressName(endpoint.String(), "glob")
@@ -39,99 +51,101 @@
 	joinLines := func(args ...string) string {
 		return strings.Join(args, "\n")
 	}
-	for _, c := range []struct {
-		globResponses   []string
-		statusResponses map[string][]interface{}
-		expectedStimuli map[string][]interface{}
-		expectedStdout  string
-		expectedStderr  string
-		expectedError   string
-	}{
-		{ // Everything succeeds.
-			[]string{"app/2", "app/1", "app/3"},
-			map[string][]interface{}{
-				"app/1": []interface{}{instanceRunning, nil, nil, nil},
-				"app/2": []interface{}{instanceNotRunning, nil},
-				"app/3": []interface{}{installationActive, nil},
+	for _, cmd := range []string{"update", "revert"} {
+		for _, c := range []struct {
+			globResponses   []string
+			statusResponses map[string][]interface{}
+			expectedStimuli map[string][]interface{}
+			expectedStdout  string
+			expectedStderr  string
+			expectedError   string
+		}{
+			{ // Everything succeeds.
+				[]string{"app/2", "app/1", "app/3"},
+				map[string][]interface{}{
+					"app/1": []interface{}{instanceRunning, nil, nil, nil},
+					"app/2": []interface{}{instanceNotRunning, nil},
+					"app/3": []interface{}{installationActive, nil},
+				},
+				map[string][]interface{}{
+					"app/1": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"},
+					"app/2": []interface{}{"Status", capitalize(cmd)},
+					"app/3": []interface{}{"Status", capitalize(cmd)},
+				},
+				joinLines(
+					fmt.Sprintf("Successful %s of version for installation \"%s/3\".", cmd, appName),
+					fmt.Sprintf("Successful %s of version for instance \"%s/1\".", cmd, appName),
+					fmt.Sprintf("Successful %s of version for instance \"%s/2\".", cmd, appName)),
+				"",
+				"",
 			},
-			map[string][]interface{}{
-				"app/1": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Update", "Run"},
-				"app/2": []interface{}{"Status", "Update"},
-				"app/3": []interface{}{"Status", "Update"},
+			{ // Assorted failure modes.
+				[]string{"app/1", "app/2", "app/3", "app/4", "app/5"},
+				map[string][]interface{}{
+					// Starts as running, fails Kill, but then
+					// recovers. This ultimately counts as a success.
+					"app/1": []interface{}{instanceRunning, fmt.Errorf("Simulate Kill failing"), instanceNotRunning, nil, nil},
+					// Starts as running, fails Kill, and stays running.
+					"app/2": []interface{}{instanceRunning, fmt.Errorf("Simulate Kill failing"), instanceRunning},
+					// Starts as running, Kill and Update succeed, but Run fails.
+					"app/3": []interface{}{instanceRunning, nil, nil, fmt.Errorf("Simulate Run failing")},
+					// Starts as running, Kill succeeds, Update fails, but Run succeeds.
+					"app/4": []interface{}{instanceRunning, nil, fmt.Errorf("Simulate %s failing", capitalize(cmd)), nil},
+					// Starts as running, Kill succeeds, Update fails, and Run fails.
+					"app/5": []interface{}{instanceRunning, nil, fmt.Errorf("Simulate %s failing", capitalize(cmd)), fmt.Errorf("Simulate Run failing")},
+				},
+				map[string][]interface{}{
+					"app/1": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Status", capitalize(cmd), "Run"},
+					"app/2": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Status"},
+					"app/3": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"},
+					"app/4": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"},
+					"app/5": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"},
+				},
+				joinLines(
+					fmt.Sprintf("Successful %s of version for instance \"%s/1\".", cmd, appName),
+					fmt.Sprintf("Successful %s of version for instance \"%s/3\".", cmd, appName),
+				),
+				joinLines(
+					fmt.Sprintf("WARNING for \"%s/1\": recovered from Kill error (device.test:<rpc.Client>\"%s/1\".Kill: Error: Simulate Kill failing). Proceeding with %s.", appName, appName, cmd),
+					fmt.Sprintf("ERROR for \"%s/2\": Kill failed: device.test:<rpc.Client>\"%s/2\".Kill: Error: Simulate Kill failing.", appName, appName),
+					fmt.Sprintf("ERROR for \"%s/3\": Run failed: device.test:<rpc.Client>\"%s/3\".Run: Error: Simulate Run failing.", appName, appName),
+					fmt.Sprintf("ERROR for \"%s/4\": %s failed: device.test:<rpc.Client>\"%s/4\".%s: Error: Simulate %s failing.", appName, capitalize(cmd), appName, capitalize(cmd), capitalize(cmd)),
+					fmt.Sprintf("ERROR for \"%s/5\": Run failed: device.test:<rpc.Client>\"%s/5\".Run: Error: Simulate Run failing.", appName, appName),
+					fmt.Sprintf("ERROR for \"%s/5\": %s failed: device.test:<rpc.Client>\"%s/5\".%s: Error: Simulate %s failing.", appName, capitalize(cmd), appName, capitalize(cmd), capitalize(cmd)),
+				),
+				"encountered a total of 4 error(s)",
 			},
-			joinLines(
-				fmt.Sprintf("Successfully updated version for installation \"%s/3\".", appName),
-				fmt.Sprintf("Successfully updated instance \"%s/1\".", appName),
-				fmt.Sprintf("Successfully updated instance \"%s/2\".", appName)),
-			"",
-			"",
-		},
-		{ // Assorted failure modes.
-			[]string{"app/1", "app/2", "app/3", "app/4", "app/5"},
-			map[string][]interface{}{
-				// Starts as running, fails Kill, but then
-				// recovers. This ultimately counts as a success.
-				"app/1": []interface{}{instanceRunning, fmt.Errorf("Simulate Kill failing"), instanceNotRunning, nil, nil},
-				// Starts as running, fails Kill, and stays running.
-				"app/2": []interface{}{instanceRunning, fmt.Errorf("Simulate Kill failing"), instanceRunning},
-				// Starts as running, Kill and Update succeed, but Run fails.
-				"app/3": []interface{}{instanceRunning, nil, nil, fmt.Errorf("Simulate Run failing")},
-				// Starts as running, Kill succeeds, Update fails, but Run succeeds.
-				"app/4": []interface{}{instanceRunning, nil, fmt.Errorf("Simulate Update failing"), nil},
-				// Starts as running, Kill succeeds, Update fails, and Run fails.
-				"app/5": []interface{}{instanceRunning, nil, fmt.Errorf("Simulate Update failing"), fmt.Errorf("Simulate Run failing")},
-			},
-			map[string][]interface{}{
-				"app/1": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Status", "Update", "Run"},
-				"app/2": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Status"},
-				"app/3": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Update", "Run"},
-				"app/4": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Update", "Run"},
-				"app/5": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Update", "Run"},
-			},
-			joinLines(
-				fmt.Sprintf("Successfully updated instance \"%s/1\".", appName),
-				fmt.Sprintf("Successfully updated instance \"%s/3\".", appName),
-			),
-			joinLines(
-				fmt.Sprintf("WARNING for \"%s/1\": recovered from Kill error (device.test:<rpc.Client>\"%s/1\".Kill: Error: Simulate Kill failing). Proceeding with update.", appName, appName),
-				fmt.Sprintf("ERROR for \"%s/2\": Kill failed: device.test:<rpc.Client>\"%s/2\".Kill: Error: Simulate Kill failing.", appName, appName),
-				fmt.Sprintf("ERROR for \"%s/3\": Run failed: device.test:<rpc.Client>\"%s/3\".Run: Error: Simulate Run failing.", appName, appName),
-				fmt.Sprintf("ERROR for \"%s/4\": Update failed: device.test:<rpc.Client>\"%s/4\".Update: Error: Simulate Update failing.", appName, appName),
-				fmt.Sprintf("ERROR for \"%s/5\": Run failed: device.test:<rpc.Client>\"%s/5\".Run: Error: Simulate Run failing.", appName, appName),
-				fmt.Sprintf("ERROR for \"%s/5\": Update failed: device.test:<rpc.Client>\"%s/5\".Update: Error: Simulate Update failing.", appName, appName),
-			),
-			"encountered a total of 4 error(s)",
-		},
-	} {
-		var stdout, stderr bytes.Buffer
-		env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
-		tapes.rewind()
-		rootTape.SetResponses(GlobResponse{c.globResponses})
-		for n, r := range c.statusResponses {
-			tapes.forSuffix(n).SetResponses(r...)
-		}
-		args := []string{"update", globName}
-		if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, args); err != nil {
-			if want, got := c.expectedError, err.Error(); want != got {
-				t.Errorf("Unexpected error: want %v, got %v", want, got)
+		} {
+			var stdout, stderr bytes.Buffer
+			env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
+			tapes.rewind()
+			rootTape.SetResponses(GlobResponse{c.globResponses})
+			for n, r := range c.statusResponses {
+				tapes.forSuffix(n).SetResponses(r...)
 			}
-		} else {
-			if c.expectedError != "" {
-				t.Errorf("Expected to get error %v, but didn't get any error.", c.expectedError)
+			args := []string{cmd, globName}
+			if err := v23cmd.ParseAndRunForTest(root, ctx, env, args); err != nil {
+				if want, got := c.expectedError, err.Error(); want != got {
+					t.Errorf("Unexpected error: want %v, got %v", want, got)
+				}
+			} else {
+				if c.expectedError != "" {
+					t.Errorf("Expected to get error %v, but didn't get any error.", c.expectedError)
+				}
 			}
-		}
 
-		if expected, got := c.expectedStdout, strings.TrimSpace(stdout.String()); got != expected {
-			t.Errorf("Unexpected stdout output from update. Got %q, expected %q", got, expected)
-		}
-		if expected, got := c.expectedStderr, strings.TrimSpace(stderr.String()); got != expected {
-			t.Errorf("Unexpected stderr output from update. Got %q, expected %q", got, expected)
-		}
-		for n, m := range c.expectedStimuli {
-			if want, got := m, tapes.forSuffix(n).Play(); !reflect.DeepEqual(want, got) {
-				t.Errorf("Unexpected stimuli for %v. Want: %v, got %v.", n, want, got)
+			if expected, got := c.expectedStdout, strings.TrimSpace(stdout.String()); got != expected {
+				t.Errorf("Unexpected stdout output from %s.\nGot:\n%v\nExpected:\n%v", cmd, got, expected)
 			}
+			if expected, got := c.expectedStderr, strings.TrimSpace(stderr.String()); got != expected {
+				t.Errorf("Unexpected stderr output from %s.\nGot:\n%v\nExpected:\n%v", cmd, got, expected)
+			}
+			for n, m := range c.expectedStimuli {
+				if want, got := m, tapes.forSuffix(n).Play(); !reflect.DeepEqual(want, got) {
+					t.Errorf("Unexpected stimuli for %v. Want: %v, got %v.", n, want, got)
+				}
+			}
+			cmd_device.ResetGlobFlags()
 		}
-		cmd_device.ResetGlobFlags()
 	}
 }
diff --git a/services/wspr/internal/rpc/server/server.go b/services/wspr/internal/rpc/server/server.go
index e3cd66e..f331764 100644
--- a/services/wspr/internal/rpc/server/server.go
+++ b/services/wspr/internal/rpc/server/server.go
@@ -206,7 +206,7 @@
 			ch <- &lib.ServerRpcReply{nil, &err, vtrace.Response{}}
 		}()
 
-		go proxyStream(call, flow.Writer, s.helper, s.helper.TypeEncoder())
+		go s.proxyStream(call, flow, s.helper, s.helper.TypeEncoder())
 
 		return replyChan
 	}
@@ -319,9 +319,10 @@
 	}
 }
 
-func proxyStream(stream rpc.Stream, w lib.ClientWriter, blessingsCache HandleStore, typeEncoder *vom.TypeEncoder) {
+func (s *Server) proxyStream(stream rpc.Stream, flow *Flow, blessingsCache HandleStore, typeEncoder *vom.TypeEncoder) {
 	var item interface{}
 	var err error
+	w := flow.Writer
 	for err = stream.Recv(&item); err == nil; err = stream.Recv(&item) {
 		if blessings, ok := item.(security.Blessings); ok {
 			item = principal.ConvertBlessingsToHandle(blessings, blessingsCache.GetOrAddBlessingsHandle(blessings))
@@ -337,7 +338,16 @@
 			return
 		}
 	}
-	vlog.Log.Errorf("Error reading from stream: %v\n", err)
+	vlog.VI(1).Infof("Error reading from stream: %v\n", err)
+	s.outstandingRequestLock.Lock()
+	_, found := s.outstandingServerRequests[flow.ID]
+	s.outstandingRequestLock.Unlock()
+
+	if !found {
+		// The flow has already been closed.  This is usually because we got a response
+		// from the javascript server.
+		return
+	}
 
 	if err := w.Send(lib.ResponseStreamClose, nil); err != nil {
 		w.Error(verror.Convert(verror.ErrInternal, nil, err))