Display errors in Syncbase debug viewer.

Assemble errors to display on web page, rather than just sending HTTP
5xx error.

Also to help testing, add a fake Syncbase API that injects errors.

(Also fix some govet and golint warnings and fix some copyright
dates.)

Change-Id: Ie0234ed6079ee63dabe527595afd76f8b851763b
diff --git a/services/debug/debug/browseserver/assets.go b/services/debug/debug/browseserver/assets.go
index 546e27c..8d6fc1b 100644
--- a/services/debug/debug/browseserver/assets.go
+++ b/services/debug/debug/browseserver/assets.go
@@ -283,7 +283,7 @@
 	return a, nil
 }
 
-var _syncbaseHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x56\x5f\x6b\x23\x37\x10\x7f\xf7\xa7\x18\x4c\x48\x93\x6b\xbd\xee\x1d\xb4\x0f\xc1\xde\x92\xc4\x57\x30\x5c\xc2\x81\xd3\xbe\x94\x7b\xd0\x4a\x63\xaf\x40\x96\x8c\x24\xbb\x98\xc5\xdf\xbd\xa3\xfd\xbf\xf6\x26\xd7\x0d\xe7\x27\x6b\xfe\xff\x7e\x33\xa3\x55\x96\x4d\x3f\x8c\x80\x7e\x8f\x66\x77\xb4\x72\x93\x7a\xf8\xf4\xeb\xc7\xdf\xe1\x25\x45\xf8\x9b\x69\x26\xe4\x7e\x0b\xf7\x7b\x9f\x1a\xeb\x22\xb8\x57\x0a\x72\x23\x07\x16\x1d\xda\x03\x8a\x28\xf7\xfe\xcb\x21\x98\x35\xf8\x54\x3a\x70\x66\x6f\x39\x02\x37\x02\x81\x8e\x1b\x73\x40\xab\x51\x40\x72\x04\x06\x0f\xab\xc5\xc4\xf9\xa3\xc2\xdc\x4d\x49\x8e\x9a\x5c\x7d\xca\x3c\x70\xa6\x21\x41\x58\x9b\xbd\x16\x20\x35\x09\x11\xbe\x2c\x1f\x3f\x3f\xaf\x3e\xc3\x5a\x2a\x8c\x46\x1f\xa6\xa7\xd3\x68\x94\x65\x02\xd7\x52\x23\x8c\xb9\xd1\x1e\xb5\x1f\x07\xe9\xcc\x21\xf7\xd2\x68\xe0\x8a\x39\x37\x1f\x97\xc7\xc9\x84\x32\x78\xb4\xb0\x15\x6a\xb2\xb1\x52\x8c\xe3\x3c\xf3\x2c\xfd\x2d\x5e\x1d\x35\x4f\x98\xc3\xd9\x94\x0e\x85\x54\xc8\x43\x15\x20\x38\x70\x24\xc0\xd5\x9f\xc9\xe4\xe3\xa7\x09\x37\xaa\x8c\x00\xb0\x22\x02\x08\x00\x8c\xb3\x2c\x7a\xb1\x88\x51\x29\x88\xfe\xdc\x2b\xf5\xcc\xb6\x78\x3a\x8d\x8b\xb0\x53\x8a\x1b\x8f\x66\xd3\xb2\xa8\x38\x80\xb0\x4c\x6f\x10\x0a\xc7\x45\xe2\x08\x03\x19\x0e\x03\x91\x65\x70\x25\x12\xb8\x9b\x43\xb4\x60\x9e\x05\x2c\xd1\x52\x40\x1e\xaa\x80\x58\x89\x43\x8d\x64\x1a\x95\x55\x35\x88\x87\x62\x06\xb8\xdf\xed\xe0\x41\xa1\x73\x52\x6f\xaa\xb0\xd5\xb9\x02\x5c\x43\x7e\x5f\x8a\x99\x50\xcd\xa1\x66\xea\xd1\x28\x55\xf0\xe1\x4a\x84\x95\xb5\x8f\x1b\xdd\x5d\xde\x8e\xa5\xa8\xa1\x92\xb6\x63\x2c\xe2\x47\x8b\xcc\x1b\xfb\x93\x0b\x63\x6b\x3b\x60\x82\x63\x0b\x0c\xa1\x10\xe7\xde\x33\x06\xa9\xc5\x35\x35\xa7\x1c\x9f\x29\xaf\x93\xff\xa1\xe7\x44\x48\x3e\x07\x68\x8b\x02\xae\x45\x32\x3f\x27\xe9\x5a\xe8\x79\xbb\x1f\xd7\x3c\xd8\x74\x73\x5f\x73\x3d\xef\x00\x89\x9f\x8c\xc5\x28\x8a\x66\x53\x16\x77\xeb\xca\x32\x54\x0e\x2f\x38\xb9\xd1\x06\x9a\xd2\xdc\x2d\x39\xf9\x8e\x93\x16\xb5\x0f\xe9\x4a\xca\x5b\x8d\x1b\xd6\xb6\xfe\xa6\x85\x1d\xdb\x58\xb3\xdf\x5d\xf6\xac\x56\xdd\x91\x79\x63\xd8\x40\xee\x69\x5d\x48\x12\xc4\x15\x4f\x39\xa6\xa0\x38\x8f\xd0\x10\x99\x73\x95\xe3\xeb\xed\x66\x19\x70\x81\x8e\x5b\xb9\x0b\x4c\xd5\x31\xc3\x3c\xac\x76\xc8\xa3\x96\xb2\x1a\x8a\xef\x06\xfc\xba\x4f\x94\x74\x29\x54\x97\x0c\x04\x4c\x9d\x72\x43\xe4\xd2\xaa\x32\x2a\x71\xbf\x99\xa0\x75\x3c\x23\xbd\x4b\xfd\x95\x67\x9b\x5f\xe0\x8a\x71\x4e\x4c\x7c\x91\xce\xe7\xf7\x44\x91\x15\xed\xd6\x41\xa7\x21\x4d\x5b\x68\x30\xc9\xf3\x74\x82\x1d\x59\x49\x22\xb1\x64\xa4\xc7\x58\xc4\x3f\x93\x75\x93\x21\x5a\xea\xb2\xfa\x0b\xe3\x2c\xfb\x57\xfa\xb4\x5d\x4e\xf4\x6c\x7c\xb0\xef\x8d\x3b\x21\x82\x5e\x0f\xd5\x9e\xdc\xb7\xa4\xcd\x54\xd7\xe7\x4e\xbc\xb2\xa8\x9e\x26\x0f\x98\x92\xa6\xd2\xde\x96\x5d\xd6\xd5\xc9\xda\x3b\x00\x6f\x8f\x54\xdb\xf6\xfd\x55\x7c\x7f\x94\xba\xf7\xaa\xbb\x1c\x82\x66\xc9\x03\x92\xd7\xae\xe7\x22\x55\x28\xf6\xee\x1f\xaa\xb2\x40\xf8\xad\xbe\x75\x73\x61\xb3\xad\xdf\x2e\x7b\xfe\x9e\xce\xb6\x69\x7b\xa2\x07\x85\x7f\x61\x09\xe5\xb8\xd8\xbf\x96\xee\xff\xde\x14\x4b\xf7\xd5\xca\x03\xf3\x97\xcb\x5c\x6b\x7e\xd8\x0a\x6f\x71\x9b\xa0\xa5\x2d\x96\x7a\x6d\xf2\xfd\x7d\xca\x25\xe7\x0c\x57\xed\x2a\xb4\xaf\xaf\x2b\x6d\x6b\x11\x92\xd6\xfb\x26\xcc\x11\x15\x6c\xac\xf4\xc7\xf0\x39\x0a\x39\xa2\xb6\x90\x8c\x1e\x94\x49\x16\x78\x78\x39\xee\xb0\x36\x69\xc9\x4e\xa7\xdb\xbe\x1d\x1d\xde\xb1\x37\x3e\x61\xae\xfe\x82\x0c\xfd\x82\xb5\xde\x5b\xad\xf8\x43\x1f\x8a\x03\xdf\x2e\x37\xcf\x06\x44\xf9\xea\x72\xb7\xe7\xaf\xa1\x59\xbb\xa2\xbc\xfa\x51\xfd\xef\xbf\x00\x00\x00\xff\xff\x5c\x44\xcd\x6b\x8b\x0b\x00\x00")
+var _syncbaseHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x56\xdf\x6b\x23\x37\x10\x7e\xf7\x5f\x31\x98\x90\x26\xd7\x7a\xdd\x3b\x68\x1f\x82\xbd\x25\x89\x53\x30\x5c\xc2\x81\xd3\xbe\x94\x7b\xd0\xae\xc6\xb6\x40\x96\x8c\x24\xbb\x18\xb3\xff\x7b\x47\xfb\x53\xbb\xb6\x93\x6e\xb8\x3c\x04\x6b\x34\x9a\xf9\x66\xbe\x6f\xb4\x3a\x1e\xc7\x9f\x06\x40\x7f\x8f\x7a\x7b\x30\x62\xb5\x76\xf0\xe5\xd7\xcf\xbf\xc3\xeb\x1a\xe1\x6f\xa6\x18\x17\xbb\x0d\xdc\xef\xdc\x5a\x1b\x1b\xc1\xbd\x94\x90\x3b\x59\x30\x68\xd1\xec\x91\x47\xf9\xe9\xbf\x2c\x82\x5e\x82\x5b\x0b\x0b\x56\xef\x4c\x8a\x90\x6a\x8e\x40\xcb\x95\xde\xa3\x51\xc8\x21\x39\x00\x83\x87\xc5\x6c\x64\xdd\x41\x62\x7e\x4c\x8a\x14\x15\x1d\x75\x6b\xe6\x20\x65\x0a\x12\x84\xa5\xde\x29\x0e\x42\x91\x11\xe1\xeb\xfc\xf1\xe9\x65\xf1\x04\x4b\x21\x31\x1a\x7c\x1a\x67\xd9\x60\x70\x3c\x72\x5c\x0a\x85\x30\x4c\xb5\x72\xa8\xdc\xd0\x5b\x27\x16\x53\x27\xb4\x82\x54\x32\x6b\xa7\xc3\x72\x39\x1a\x51\x06\x87\x06\x36\x5c\x8e\x56\x46\xf0\x61\x9c\x67\x9e\xac\x7f\x8b\x17\x07\x95\x26\xcc\xe2\x64\x4c\x8b\xc2\xca\xc5\xbe\x0a\xe0\x0f\xa4\x48\x05\x57\x3f\x46\xa3\xcf\x5f\x46\xa9\x96\x65\x04\x80\x05\x35\x80\x0a\x80\xe1\xf1\x18\xbd\x1a\xc4\xa8\x34\x44\x7f\xee\xa4\x7c\x61\x1b\xcc\xb2\x61\x11\x76\x4c\x71\xe3\xc1\x64\x5c\x82\x8a\x7d\x11\x86\xa9\x15\x42\x71\x70\x96\x58\xaa\x81\x1c\xfb\x15\x71\x3c\xc2\x15\x4f\xe0\x6e\x0a\xd1\x8c\x39\xe6\x6b\x89\xe6\x1c\xf2\x50\x45\x89\x95\xd9\x63\x24\xd7\xa8\x44\xd5\x54\xdc\xb7\x66\x80\xfb\xed\x16\x1e\x24\x5a\x2b\xd4\xaa\x0a\x5b\xad\xab\x82\xeb\x92\x8b\x45\x5d\xec\x93\x31\xb6\x44\xf7\x91\xd4\xfe\x84\x8c\x27\xdc\xc5\x14\x48\x1b\xca\xe1\x68\xc5\x63\xea\x7f\x6e\xc8\x32\x32\xf1\x98\xfe\xc9\xe6\x50\x07\x09\x2a\x5e\x23\xe8\x9b\x7f\x12\xc6\xad\x8b\x7a\xd4\x52\x16\x3c\x85\xb5\x79\x6f\x17\x37\x7b\x77\xb9\x4c\xe6\xbc\xa6\x80\x76\x5b\xce\x3c\x7e\x34\xc8\x9c\x36\x3f\x59\x3f\x4e\xa6\xd5\x64\x7f\x30\x68\x72\x5e\x66\xe7\xf4\x84\xc1\xda\xe0\x92\x44\x53\xca\x7a\x9c\xd6\xc9\xff\x50\x53\x22\x2a\xd7\x27\x9a\x02\xc0\x35\x4f\xa6\x5d\xf2\xae\xb9\x9a\x86\x3a\xb9\x4e\xbd\x4f\x3b\xf7\x75\xaa\xa6\xad\x42\xe2\x67\x6d\x30\x8a\xa2\xc9\x98\xc5\x6d\x5c\xd4\x6c\x69\xf1\xa4\x27\x37\x4a\x43\x03\xcd\xde\xe6\x34\x86\x87\x42\x86\x6a\x2a\x03\x1a\xfb\xd1\x76\x9e\x34\x3f\xfb\x2b\xa3\x77\xdb\x53\xce\xea\xad\x3b\x72\x6f\x1c\x9b\x92\x3b\xd4\x5d\x52\x77\x15\xef\x4d\xad\xb6\xe2\x84\x95\xd7\xb4\x96\x8a\xaf\x18\x08\x03\xb5\xb0\x35\x14\x35\x43\x70\x56\x27\x65\xc0\x19\xda\xd4\x88\xad\xe7\xa0\x8e\xe9\x95\xb6\xd8\x62\x1a\x05\x9b\x95\xdc\xde\x0d\xf8\x6d\x97\x48\x61\xd7\x50\x5d\xab\xe0\xbb\xd5\x82\xeb\x23\x97\x5e\x95\x53\xd9\xd1\x37\x13\x74\x3a\x2a\xdb\x86\x86\x80\x2b\xc7\x56\xbf\xc0\x15\x4b\x53\xea\xc4\x57\x61\x5d\x7e\x33\x16\x59\xd1\x6c\x2c\x74\xc8\xa9\x08\x22\xc9\xd3\xc9\x2c\x83\x2d\x79\x09\x6a\x62\xd9\x91\x33\xce\x3c\xfe\x99\xbc\x9b\x0c\xd1\x5c\x9d\x12\x59\xa1\xfa\x57\xb8\x75\x08\x27\x7a\xd1\xce\xfb\x9f\x8d\x3b\xa2\x06\x5d\x0e\xd5\x55\xc6\x25\x6b\xfb\xea\xcb\xd7\x1d\x8d\xe5\xa0\xce\x90\xdc\x43\x25\x0d\xd2\xb3\x94\x9d\xe2\x6a\x65\x3d\x2b\x80\xb7\x25\x15\xfa\x7e\x1c\xc5\xfb\x52\x6a\xdf\xd8\xf6\x54\x04\xcd\xf5\xe1\x2b\xb9\x74\xf1\x17\xa9\x3c\xd8\xbb\x7f\x08\x65\x51\xe1\xf7\xfa\x3e\xcf\x8d\xcd\xb4\x7e\x3f\xe5\xfc\x23\xcc\x86\x6d\x7b\xa6\x27\x94\x7b\x65\x09\xe5\x38\x99\xbf\x60\xef\xff\xde\x14\x73\xfb\xcd\x88\x3d\x73\xa7\xc3\x5c\xef\xfc\xb0\x11\xde\xe0\x26\x41\x43\x53\x2c\xd4\x52\xe7\xf3\xfb\x9c\x5b\xba\x1d\xae\xe8\x2a\x76\x2f\x8f\x2b\x4d\x6b\x11\x92\xc6\xfb\xc6\xeb\x88\x00\x6b\x23\xdc\xc1\x7f\xe8\x7c\x8e\x28\x34\x92\xd3\x83\xd4\xc9\x0c\xf7\xaf\x87\x2d\xd6\x2e\x81\x2d\xcb\x6e\xcf\xcd\x68\x7f\xc6\xde\xf8\x38\xda\xfa\xdb\xd4\xf7\xdb\x18\xbc\x30\x83\xf8\x7d\x9f\xc6\x3d\x5f\x45\x37\x2f\x1a\x78\xf9\xce\xb4\xb7\xdd\xf7\xdf\x24\x44\x94\xa3\x1f\xd4\xbf\xfe\x0b\x00\x00\xff\xff\x66\x98\x51\x96\x7d\x0c\x00\x00")
 
 func syncbaseHtmlBytes() ([]byte, error) {
 	return bindataRead(
diff --git a/services/debug/debug/browseserver/assets/syncbase.html b/services/debug/debug/browseserver/assets/syncbase.html
index 1831f83..9f9b4d0 100644
--- a/services/debug/debug/browseserver/assets/syncbase.html
+++ b/services/debug/debug/browseserver/assets/syncbase.html
@@ -20,6 +20,11 @@
       <div class="mdl-cell mdl-cell--12-col">
         App Blessing "{{$db.Blessing}}"
       </div>
+      {{range .Errs}}
+        <div class="mdl-cell mdl-cell--12-col">
+          <dl><dt>Error</dt><dd>{{.Error}}</dd></dl>
+        </div>
+      {{end}}
       <div class="mdl-cell mdl-cell--12-col">
         <dl>
         {{range .Collections}}
@@ -35,6 +40,9 @@
       <dl>
         {{range .Syncgroups}}
           <dt>Syncgroup: {{.Syncgroup.Id.Name}}<dt>
+          {{range .Errs}}
+            <dt>Error</dt><dd>{{.Error}}</dd>
+          {{end}}
           <dd><dl><dt>Blessing</dt><dd>{{.Syncgroup.Id.Blessing}}</dd></dl></dd>
           <dd><dl><dt>Description</dt><dd>"{{.Spec.Description}}"</dd></dl></dd>
           <dd><dl><dt>Publish Syncbase Name</dt><dd>{{.Spec.PublishSyncbaseName}}</dd></dl></dd>
diff --git a/services/debug/debug/browseserver/browseserver.go b/services/debug/debug/browseserver/browseserver.go
index d770868..a59d12a 100644
--- a/services/debug/debug/browseserver/browseserver.go
+++ b/services/debug/debug/browseserver/browseserver.go
@@ -8,6 +8,7 @@
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"html"
 	"html/template"
@@ -25,16 +26,19 @@
 	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/naming"
+	"v.io/v23/rpc/reserved"
 	"v.io/v23/security"
 	"v.io/v23/services/logreader"
 	"v.io/v23/services/stats"
 	svtrace "v.io/v23/services/vtrace"
+	"v.io/v23/syncbase"
 	"v.io/v23/uniqueid"
 	"v.io/v23/verror"
 	"v.io/v23/vom"
 	"v.io/v23/vtrace"
 	"v.io/x/ref/services/debug/debug/browseserver/sbtree"
 	"v.io/x/ref/services/internal/pproflib"
+	"v.io/x/ref/services/syncbase/fake"
 )
 
 //go:generate ./gen_assets.sh
@@ -837,7 +841,8 @@
 
 // The syncbaseHandler handles the main Syncbase viewer page is linked from the
 // nav bar of the Debug browser. It displays a list of databases and their
-// collections, and has links to the detailed collection page.
+// collections, and has links to the detailed collection page.  It supports
+// adding a "&fake=true" query argument to inject a fake Syncbase service.
 type syncbaseHandler struct{ *handler }
 
 func internalServerError(w http.ResponseWriter, doing string, err error) {
@@ -853,6 +858,7 @@
 func (h *syncbaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	var (
 		server = r.FormValue("n")
+		fakeIt = len(r.FormValue("fake")) > 0 // for testing
 	)
 	ctx, tracer := newTracer(h.ctx)
 	if len(server) == 0 {
@@ -860,28 +866,33 @@
 		return
 	}
 
-	sbTree, err := sbtree.AssembleSyncbaseTree(ctx, server)
-
+	service, hasSyncbase, err := syncbaseService(ctx, server, fakeIt)
 	if err != nil {
-		if err == sbtree.NoSyncbaseError {
-			// Error because no Syncbase, send to no-syncbase page.
-			args := struct {
-				ServerName  string
-				CommandLine string
-				Vtrace      *Tracer
-			}{
-				ServerName:  server,
-				CommandLine: "(no command line)",
-				Vtrace:      tracer,
-			}
-			h.execute(h.ctx, w, r, noSyncbaseTmpl, args)
-		} else {
-			// Some other error.
-			internalServerError(w, "getting syncbase information", err)
-		}
+		internalServerError(w, "getting interfaces", err)
 		return
 	}
 
+	if !hasSyncbase {
+		args := struct {
+			ServerName  string
+			CommandLine string
+			Vtrace      *Tracer
+		}{
+			ServerName:  server,
+			CommandLine: "(no command line)",
+			Vtrace:      tracer,
+		}
+		h.execute(h.ctx, w, r, noSyncbaseTmpl, args)
+		return
+	}
+
+	dbIds, err := service.ListDatabases(ctx)
+	if err != nil {
+		internalServerError(w, "listing databases", err)
+		return
+	}
+	sbTree := sbtree.AssembleSyncbaseTree(ctx, server, service, dbIds)
+
 	args := struct {
 		ServerName  string
 		CommandLine string
@@ -896,6 +907,44 @@
 	h.execute(h.ctx, w, r, syncbaseTmpl, args)
 }
 
+func syncbaseService(ctx *context.T, server string, fakeIt bool) (service syncbase.Service, hasSyncbase bool, err error) {
+	if fakeIt {
+		service = fake.Service(
+			errors.New("pretend we cannot list collections"),
+			errors.New("pretend we cannot get syncgroup spec"),
+		)
+		hasSyncbase = true
+		return
+	}
+	hasSyncbase, err = hasSyncbaseService(ctx, server)
+	if err != nil {
+		return
+	}
+	if hasSyncbase {
+		service = syncbase.NewService(server)
+	}
+	return
+}
+
+// hasSyncbaseService determines whether the given server implements the
+// Syncbase interface.
+func hasSyncbaseService(ctx *context.T, server string) (bool, error) {
+	const (
+		syncbasePkgPath = "v.io/v23/services/syncbase"
+		syncbaseName    = "Service"
+	)
+	interfaces, err := reserved.Signature(ctx, server)
+	if err != nil {
+		return false, err
+	}
+	for _, ifc := range interfaces {
+		if ifc.Name == syncbaseName && ifc.PkgPath == syncbasePkgPath {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
 // collectionHandler handles the Collections details page that is linked off the
 // main Syncbase viewer page.
 type collectionHandler struct{ *handler }
diff --git a/services/debug/debug/browseserver/sbtree/colltree.go b/services/debug/debug/browseserver/sbtree/colltree.go
index 5fd2c37..561ae5d 100644
--- a/services/debug/debug/browseserver/sbtree/colltree.go
+++ b/services/debug/debug/browseserver/sbtree/colltree.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Copyright 2016 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.
 
@@ -42,13 +42,13 @@
 	ctx *context.T, server, dbBlessing, dbName, collBlessing, collName, firstKey string, keysPerPage int,
 ) *CollectionTree {
 	var (
-		dbId    = wire.Id{dbBlessing, dbName}
-		collId  = wire.Id{collBlessing, collName}
+		dbID    = wire.Id{Blessing: dbBlessing, Name: dbName}
+		collID  = wire.Id{Blessing: collBlessing, Name: collName}
 		service = syncbase.NewService(server)
 
 		// TODO(eobrain) Confirm nil for schema is appropriate
-		db   = service.DatabaseForId(dbId, nil)
-		coll = db.CollectionForId(collId)
+		db   = service.DatabaseForId(dbID, nil)
+		coll = db.CollectionForId(collID)
 	)
 
 	rowCount, totKeySize, page := scanCollection(ctx, coll, firstKey, keysPerPage)
diff --git a/services/debug/debug/browseserver/sbtree/colltree_test.go b/services/debug/debug/browseserver/sbtree/colltree_test.go
index 921e0db..3c1bd8a 100644
--- a/services/debug/debug/browseserver/sbtree/colltree_test.go
+++ b/services/debug/debug/browseserver/sbtree/colltree_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Copyright 2016 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.
 
diff --git a/services/debug/debug/browseserver/sbtree/sbtree.go b/services/debug/debug/browseserver/sbtree/sbtree.go
index 96df9f5..65efd00 100644
--- a/services/debug/debug/browseserver/sbtree/sbtree.go
+++ b/services/debug/debug/browseserver/sbtree/sbtree.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Copyright 2016 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.
 
@@ -13,11 +13,9 @@
 package sbtree
 
 import (
-	"errors"
 	"fmt"
 
 	"v.io/v23/context"
-	"v.io/v23/rpc/reserved"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase"
 )
@@ -32,92 +30,70 @@
 	Database    syncbase.Database
 	Collections []syncbase.Collection
 	Syncgroups  []syncgroupTree
+	Errs        []error
 }
 
 type syncgroupTree struct {
 	Syncgroup syncbase.Syncgroup
 	Spec      wire.SyncgroupSpec
 	Members   map[string]wire.SyncgroupMemberInfo
+	Errs      []error
 }
 
-// NoSyncbaseError returned as error from AssembleSbTree when the given server
-// does not implement the Syncbase RPC interface.
-var NoSyncbaseError = errors.New("Server does not have Syncbase")
-
-// AssembleSbTree returns information describing the Syncbase server running on
-// the given server. One possible error it can return is NoSyncbaseError,
-// indicating that the server does not implement the Syncbase RPC interface.
-func AssembleSyncbaseTree(ctx *context.T, server string) (*SyncbaseTree, error) {
-	hasSyncbase, err := hasSyncbaseService(ctx, server)
-	if err != nil {
-		return nil, fmt.Errorf("Problem getting interfaces: %v", err)
-	}
-	if !hasSyncbase {
-		return nil, NoSyncbaseError
-	}
-
-	service := syncbase.NewService(server)
-
-	dbIds, err := service.ListDatabases(ctx)
-	if err != nil {
-		return nil, fmt.Errorf("Problem listing databases: %v", err)
-	}
-
+// AssembleSyncbaseTree returns information describing the Syncbase server
+// running on the given server. Errors are included in the tree, so they can be
+// displayed in the HTML.
+func AssembleSyncbaseTree(
+	ctx *context.T, server string, service syncbase.Service, dbIds []wire.Id,
+) *SyncbaseTree {
 	dbTrees := make([]dbTree, len(dbIds))
 	for i := range dbIds {
+		dbTrees[i] = dbTree{}
+
 		// TODO(eobrain) Confirm nil for schema is appropriate
 		db := service.DatabaseForId(dbIds[i], nil)
+		dbTrees[i].Database = db
 
 		// Assemble collections
 		collIds, err := db.ListCollections(ctx)
-		if err != nil {
-			return nil, fmt.Errorf("Problem listing collections: %v", err)
-		}
-		colls := make([]syncbase.Collection, len(collIds))
-		for j := range collIds {
-			colls[j] = db.CollectionForId(collIds[j])
+		if err == nil {
+			dbTrees[i].Collections = make([]syncbase.Collection, len(collIds))
+			for j := range collIds {
+				dbTrees[i].Collections[j] = db.CollectionForId(collIds[j])
+			}
+		} else {
+			dbTrees[i].Errs = append(dbTrees[i].Errs,
+				fmt.Errorf("Problem listing collections: %v", err))
 		}
 
 		// Assemble syncgroups
 		sgIds, err := db.ListSyncgroups(ctx)
 		if err != nil {
-			return nil, fmt.Errorf("Problem listing syncgroups: %v", err)
+			dbTrees[i].Errs = append(dbTrees[i].Errs, err)
+			continue
 		}
 		sgs := make([]syncgroupTree, len(sgIds))
+		dbTrees[i].Syncgroups = sgs
 		for j := range sgIds {
+			sgs[j] = syncgroupTree{}
 			sg := db.SyncgroupForId(sgIds[j])
+			sgs[j].Syncgroup = sg
 			spec, _, err := sg.GetSpec(ctx)
 			if err != nil {
-				return nil, fmt.Errorf("Problem getting spec of syncgroup: %v", err)
+				sgs[j].Errs = append(sgs[j].Errs,
+					fmt.Errorf("Problem getting spec of syncgroup: %v", err))
+				continue
 			}
+			sgs[j].Spec = spec
 			members, err := sg.GetMembers(ctx)
 			if err != nil {
-				return nil, fmt.Errorf("Problem getting members of syncgroup: %v", err)
+				sgs[j].Errs = append(sgs[j].Errs,
+					fmt.Errorf("Problem getting members of syncgroup: %v", err))
+				continue
 			}
-			sgs[j] = syncgroupTree{sg, spec, members}
-		}
-
-		dbTrees[i] = dbTree{db, colls, sgs}
-	}
-
-	return &SyncbaseTree{service, dbTrees}, nil
-}
-
-// hasSyncbaseService determines whether the given server implements the
-// Syncbase interface.
-func hasSyncbaseService(ctx *context.T, server string) (bool, error) {
-	const (
-		syncbasePkgPath = "v.io/v23/services/syncbase"
-		syncbaseName    = "Service"
-	)
-	interfaces, err := reserved.Signature(ctx, server)
-	if err != nil {
-		return false, err
-	}
-	for _, ifc := range interfaces {
-		if ifc.Name == syncbaseName && ifc.PkgPath == syncbasePkgPath {
-			return true, nil
+			sgs[j].Members = members
 		}
 	}
-	return false, nil
+
+	return &SyncbaseTree{service, dbTrees}
 }
diff --git a/services/debug/debug/browseserver/sbtree/sbtree_test.go b/services/debug/debug/browseserver/sbtree/sbtree_test.go
index 0c0f4f3..1d23928 100644
--- a/services/debug/debug/browseserver/sbtree/sbtree_test.go
+++ b/services/debug/debug/browseserver/sbtree/sbtree_test.go
@@ -1,42 +1,32 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Copyright 2016 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 sbtree_test
 
 import (
+	"errors"
 	"testing"
 
+	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase"
 	_ "v.io/x/ref/runtime/factories/generic"
 	"v.io/x/ref/services/debug/debug/browseserver/sbtree"
+	"v.io/x/ref/services/syncbase/fake"
 	tu "v.io/x/ref/services/syncbase/testutil"
-	"v.io/x/ref/test"
 )
 
-func TestWithNoServer(t *testing.T) {
-	if testing.Short() {
-		t.Skip("skipping test, because has long timeout.")
-	}
-	ctx, cleanup := test.V23Init()
-	defer cleanup()
-
-	_, err := sbtree.AssembleSyncbaseTree(ctx, "no-such-server")
-
-	if err == nil || err == sbtree.NoSyncbaseError {
-		t.Errorf("Got %v, want not nil and not %v", err, sbtree.NoSyncbaseError)
-	}
-}
-
 func TestWithEmptyService(t *testing.T) {
 	ctx, serverName, cleanup := tu.SetupOrDie(nil)
 	defer cleanup()
-
-	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	service := syncbase.NewService(serverName)
+	dbIds, err := service.ListDatabases(ctx)
 	if err != nil {
 		t.Fatal(err)
 	}
 
+	got := sbtree.AssembleSyncbaseTree(ctx, serverName, service, dbIds)
+
 	if got.Service.FullName() != serverName {
 		t.Errorf("got %q, want %q", got.Service.FullName(), serverName)
 	}
@@ -55,11 +45,15 @@
 	for _, dbName := range dbNames {
 		tu.CreateDatabase(t, ctx, service, dbName)
 	}
-
-	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	dbIds, err := service.ListDatabases(ctx)
 	if err != nil {
 		t.Fatal(err)
 	}
+	if len(dbIds) != 3 {
+		t.Fatalf("Got %d, want 3", len(dbIds))
+	}
+
+	got := sbtree.AssembleSyncbaseTree(ctx, serverName, service, dbIds)
 
 	if len(got.Dbs) != 3 {
 		t.Fatalf("want 3 databases, got %v", got.Dbs)
@@ -74,6 +68,9 @@
 		if len(db.Syncgroups) != 0 {
 			t.Errorf("want no syncgroups, got %v", db.Syncgroups)
 		}
+		if len(db.Errs) != 0 {
+			t.Errorf("want no errors, got %v", db.Errs)
+		}
 	}
 }
 
@@ -88,11 +85,15 @@
 	for _, collName := range collNames {
 		tu.CreateCollection(t, ctx, database, collName)
 	}
-
-	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	dbIds, err := service.ListDatabases(ctx)
 	if err != nil {
 		t.Fatal(err)
 	}
+	if len(dbIds) != 1 {
+		t.Fatalf("Got %d, want 1", len(dbIds))
+	}
+
+	got := sbtree.AssembleSyncbaseTree(ctx, serverName, service, dbIds)
 
 	if len(got.Dbs) != 1 {
 		t.Fatalf("want 1 database, got %v", got.Dbs)
@@ -125,10 +126,15 @@
 		tu.CreateSyncgroup(t, ctx, database, coll, sgName, sgDescriptions[i])
 	}
 
-	got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+	dbIds, err := service.ListDatabases(ctx)
 	if err != nil {
 		t.Fatal(err)
 	}
+	if len(dbIds) != 1 {
+		t.Fatalf("Got %d, want 1", len(dbIds))
+	}
+
+	got := sbtree.AssembleSyncbaseTree(ctx, serverName, service, dbIds)
 
 	if len(got.Dbs) != 1 {
 		t.Fatalf("want 1 database, got %v", got.Dbs)
@@ -144,5 +150,82 @@
 			t.Errorf(`got %q, want "the_collection"`,
 				sg.Spec.Collections[0].Name)
 		}
+		if len(sg.Errs) != 0 {
+			t.Errorf("want no errors, got %v", sg.Errs)
+		}
+	}
+}
+
+func TestWithNoErrs(t *testing.T) {
+	service := fake.Service(nil, nil)
+	dbIds := []wire.Id{wire.Id{}}
+
+	got := sbtree.AssembleSyncbaseTree(nil, "", service, dbIds)
+
+	if len(got.Dbs) != 1 {
+		t.Fatalf("want 1 database, got %v", got.Dbs)
+	}
+	if len(got.Dbs[0].Errs) != 0 {
+		t.Fatalf("want 0 error, got %v", got.Dbs[0].Errs)
+	}
+}
+
+func TestWithListCollectionsErr(t *testing.T) {
+	service := fake.Service(errors.New("<<injected list-collections error>>"), nil)
+	dbIds := []wire.Id{wire.Id{}}
+
+	got := sbtree.AssembleSyncbaseTree(nil, "", service, dbIds)
+
+	if len(got.Dbs) != 1 {
+		t.Fatalf("want 1 database, got %v", got.Dbs)
+	}
+	if len(got.Dbs[0].Errs) != 1 {
+		t.Fatalf("want 1 error, got %v", got.Dbs[0].Errs)
+	}
+	want := "Problem listing collections: <<injected list-collections error>>"
+	if got.Dbs[0].Errs[0].Error() != want {
+		t.Fatalf("got %v, want %q", got.Dbs[0].Errs, want)
+	}
+}
+
+func TestWithNoSyncgroupErrs(t *testing.T) {
+	service := fake.Service(nil, nil)
+	dbIds := []wire.Id{wire.Id{}}
+
+	got := sbtree.AssembleSyncbaseTree(nil, "", service, dbIds)
+
+	if len(got.Dbs) != 1 {
+		t.Fatalf("want 1 database, got %v", got.Dbs)
+	}
+	if len(got.Dbs[0].Syncgroups) != 1 {
+		t.Fatalf("want 1 syncgroups, got %v", got.Dbs[0].Syncgroups)
+	}
+
+	sg := got.Dbs[0].Syncgroups[0]
+	if len(sg.Errs) != 0 {
+		t.Fatalf("want 0 error, got %v", sg.Errs)
+	}
+}
+
+func TestWithSyncgroupErr(t *testing.T) {
+	service := fake.Service(nil, errors.New("<<injected syncgroup spec error>>"))
+	dbIds := []wire.Id{wire.Id{}}
+
+	got := sbtree.AssembleSyncbaseTree(nil, "", service, dbIds)
+
+	if len(got.Dbs) != 1 {
+		t.Fatalf("want 1 database, got %v", got.Dbs)
+	}
+	if len(got.Dbs[0].Syncgroups) != 1 {
+		t.Fatalf("want 1 syncgroups, got %v", got.Dbs[0].Syncgroups)
+	}
+
+	sg := got.Dbs[0].Syncgroups[0]
+	if len(sg.Errs) != 1 {
+		t.Fatalf("want 1 error, got %v", sg.Errs)
+	}
+	want := "Problem getting spec of syncgroup: <<injected syncgroup spec error>>"
+	if sg.Errs[0].Error() != want {
+		t.Fatalf("got %v, want %q", sg.Errs, want)
 	}
 }
diff --git a/services/syncbase/fake/common.go b/services/syncbase/fake/common.go
new file mode 100644
index 0000000..9852a1f
--- /dev/null
+++ b/services/syncbase/fake/common.go
@@ -0,0 +1,22 @@
+// Copyright 2016 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 fake
+
+import (
+	"v.io/v23/context"
+	"v.io/v23/security/access"
+	wire "v.io/v23/services/syncbase"
+)
+
+// common implements some common functionality of the other fakes.
+type common struct{}
+
+func (*common) SetPermissions(*context.T, access.Permissions, string) error { return nil }
+
+func (*common) GetPermissions(*context.T) (access.Permissions, string, error) {
+	return nil, "", nil
+}
+func (*common) Id() wire.Id                  { return wire.Id{Blessing: "a-blessing", Name: "a-name"} }
+func (*common) Destroy(ctx *context.T) error { return nil }
diff --git a/services/syncbase/fake/database.go b/services/syncbase/fake/database.go
new file mode 100644
index 0000000..9ff559e
--- /dev/null
+++ b/services/syncbase/fake/database.go
@@ -0,0 +1,64 @@
+// Copyright 2016 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 fake
+
+import (
+	"v.io/v23/context"
+	"v.io/v23/security/access"
+	wire "v.io/v23/services/syncbase"
+	"v.io/v23/services/watch"
+	"v.io/v23/syncbase"
+)
+
+// database implements the syncbase.Database interface.
+type database struct {
+	common
+	listCollectionsErr error
+	specErr            error
+}
+
+func (*database) FullName() string                                  { return "the-database" }
+func (*database) Collection(*context.T, string) syncbase.Collection { return nil }
+func (*database) CollectionForId(id wire.Id) syncbase.Collection    { return nil }
+func (db *database) ListCollections(ctx *context.T) ([]wire.Id, error) {
+	return nil, db.listCollectionsErr
+}
+func (*database) Exec(
+	*context.T, string, ...interface{},
+) ([]string, syncbase.ResultStream, error) {
+	return nil, nil, nil
+}
+func (*database) GetResumeMarker(*context.T) (watch.ResumeMarker, error) {
+	return nil, nil
+}
+func (*database) Create(*context.T, access.Permissions) error { return nil }
+func (*database) Exists(*context.T) (bool, error) {
+	return true, nil
+}
+func (*database) BeginBatch(
+	*context.T, wire.BatchOptions,
+) (syncbase.BatchDatabase, error) {
+	return nil, nil
+}
+func (
+	*database) Watch(*context.T, watch.ResumeMarker, []wire.CollectionRowPattern,
+) syncbase.WatchStream {
+	return nil
+}
+func (db *database) SyncgroupForId(wire.Id) syncbase.Syncgroup {
+	return &syncgroup{specErr: db.specErr}
+}
+func (*database) Syncgroup(*context.T, string) syncbase.Syncgroup {
+	return nil
+}
+func (*database) ListSyncgroups(ctx *context.T) ([]wire.Id, error) {
+	return []wire.Id{wire.Id{}}, nil
+}
+func (*database) CreateBlob(*context.T) (syncbase.Blob, error) { return nil, nil }
+func (*database) Blob(wire.BlobRef) syncbase.Blob              { return nil }
+func (*database) EnforceSchema(*context.T) error               { return nil }
+func (*database) PauseSync(*context.T) error                   { return nil }
+func (*database) ResumeSync(*context.T) error                  { return nil }
+func (*database) Close()                                       {}
diff --git a/services/syncbase/fake/service.go b/services/syncbase/fake/service.go
new file mode 100644
index 0000000..cc94d6a
--- /dev/null
+++ b/services/syncbase/fake/service.go
@@ -0,0 +1,41 @@
+// Copyright 2016 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 fake provides a fake version of syncbase API for testing
+// with error conditions.
+package fake
+
+import (
+	"v.io/v23/context"
+	wire "v.io/v23/services/syncbase"
+	"v.io/v23/syncbase"
+)
+
+// Service returns a fake implementation, returning the given errors (which may
+// be nil).
+func Service(listCollectionsErr, specErr error) syncbase.Service {
+	return &service{listCollectionsErr: listCollectionsErr, specErr: specErr}
+}
+
+// service implements the syncbase.Service interface.
+type service struct {
+	common
+	listCollectionsErr error
+	specErr            error
+}
+
+func (*service) FullName() string { return "the-service-name" }
+
+func (s *service) Database(*context.T, string, *syncbase.Schema) syncbase.Database {
+	return &database{
+		listCollectionsErr: s.listCollectionsErr,
+		specErr:            s.specErr,
+	}
+}
+
+func (s *service) DatabaseForId(wire.Id, *syncbase.Schema) syncbase.Database {
+	return s.Database(nil, "", nil)
+}
+
+func (*service) ListDatabases(*context.T) ([]wire.Id, error) { return []wire.Id{wire.Id{}}, nil }
diff --git a/services/syncbase/fake/syncgroup.go b/services/syncbase/fake/syncgroup.go
new file mode 100644
index 0000000..04c4285
--- /dev/null
+++ b/services/syncbase/fake/syncgroup.go
@@ -0,0 +1,36 @@
+// Copyright 2016 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 fake
+
+import (
+	"v.io/v23/context"
+	wire "v.io/v23/services/syncbase"
+)
+
+// syncgroup implements the syncbase.Syncgroup interface.
+type syncgroup struct {
+	common
+	specErr error
+}
+
+func (*syncgroup) Create(*context.T, wire.SyncgroupSpec, wire.SyncgroupMemberInfo) error {
+	return nil
+}
+func (*syncgroup) Join(
+	*context.T, string, []string, wire.SyncgroupMemberInfo) (wire.SyncgroupSpec, error,
+) {
+	return wire.SyncgroupSpec{}, nil
+}
+func (*syncgroup) Leave(*context.T) error         { return nil }
+func (*syncgroup) Eject(*context.T, string) error { return nil }
+func (sg *syncgroup) GetSpec(*context.T) (wire.SyncgroupSpec, string, error) {
+	return wire.SyncgroupSpec{}, "", sg.specErr
+}
+func (*syncgroup) SetSpec(*context.T, wire.SyncgroupSpec, string) error {
+	return nil
+}
+func (*syncgroup) GetMembers(*context.T) (map[string]wire.SyncgroupMemberInfo, error) {
+	return nil, nil
+}