diff --git a/.travis.yml b/.travis.yml index 43f935f7..aa279034 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,15 @@ language: go go: -- 1.9 -- tip + - 1.11 + - tip +cache: + directories: + - $HOME/.cache/go-build + - $HOME/gopath/pkg/mod matrix: allow_failures: - go: tip script: - go test -v -race -cpu=1,2,4 ./... + - env GO111MODULE=on go build ./... + - env GO111MODUKE=on go test -v -race -cpu=1,2,4 ./... + - env GO111MODUKE=on go test -v -race -cpu=1,2,4 ./examples/... diff --git a/example_test.go b/example_test.go index 44f9cdf4..72fd10f9 100644 --- a/example_test.go +++ b/example_test.go @@ -1,26 +1,95 @@ -// +build go1.7 - package restlayer import ( "context" + "fmt" + "log" "net/http" "net/url" + "os" "time" - "github.com/justinas/alice" "github.com/rs/cors" - "github.com/rs/rest-layer/resource/testing/mem" + "github.com/rs/rest-layer/resource" + "github.com/rs/rest-layer/resource/testing/mem" "github.com/rs/rest-layer/rest" "github.com/rs/rest-layer/schema" - "github.com/rs/xaccess" - "github.com/rs/zerolog" - "github.com/rs/zerolog/hlog" - "github.com/rs/zerolog/log" ) +// ResponseRecorder extends http.ResponseWriter with the ability to capture +// the status and number of bytes written +type ResponseRecorder struct { + http.ResponseWriter + + statusCode int + length int +} + +// NewResponseRecorder returns a ResponseRecorder that wraps w. +func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder { + return &ResponseRecorder{ResponseWriter: w, statusCode: http.StatusOK} +} + +// Write writes b to the underlying response writer and stores how many bytes +// have been written. +func (w *ResponseRecorder) Write(b []byte) (n int, err error) { + n, err = w.ResponseWriter.Write(b) + w.length += n + return +} + +// WriteHeader stores and writes the HTTP status code. +func (w *ResponseRecorder) WriteHeader(code int) { + w.statusCode = code + w.ResponseWriter.WriteHeader(code) +} + +// StatusCode returns the status-code written to the response or 200 (OK). +func (w *ResponseRecorder) StatusCode() int { + if w.statusCode == 0 { + return http.StatusOK + } + return w.statusCode +} + +func AccessLog(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rec := NewResponseRecorder(w) + + next.ServeHTTP(rec, r) + status := rec.StatusCode() + length := rec.length + + // In this example we use the standard library logger. Structured logs + // may prove more parsable in a production environment. + log.Printf("D! Served HTTP Request %s %s with Response %d %s [%d bytes] in %d ms", + r.Method, + r.URL, + status, + http.StatusText(status), + length, + time.Since(start).Nanoseconds()/1e6, + ) + }) +} + +var logLevelPrefixes = map[resource.LogLevel]string{ + resource.LogLevelFatal: "E!", + resource.LogLevelError: "E!", + resource.LogLevelWarn: "W!", + resource.LogLevelInfo: "I!", + resource.LogLevelDebug: "D!", +} + func Example() { + // Configure a log-addapter for the resource pacakge. + resource.LoggerLevel = resource.LogLevelDebug + resource.Logger = func(ctx context.Context, level resource.LogLevel, msg string, fields map[string]interface{}) { + fmt.Printf("%s %s %v", logLevelPrefixes[level], msg, fields) + } + var ( // Define a user resource schema user = schema.Schema{ @@ -115,12 +184,12 @@ func Example() { } ) - // Create a REST API root resource + // Create a REST API root resource. index := resource.NewIndex() // Add a resource on /users[/:user_id] users := index.Bind("users", user, mem.NewHandler(), resource.Conf{ - // We allow all REST methods + // We allow all REST methods. // (rest.ReadWrite is a shortcut for []rest.Mode{Create, Read, Update, Delete, List}) AllowedModes: resource.ReadWrite, }) @@ -132,53 +201,32 @@ func Example() { AllowedModes: []resource.Mode{resource.Read, resource.List, resource.Create, resource.Delete}, }) - // Add a friendly alias to public posts + // Add a friendly alias to public posts. // (equivalent to /users/:user_id/posts?filter={"public":true}) posts.Alias("public", url.Values{"filter": []string{"{\"public\"=true}"}}) - // Create API HTTP handler for the resource graph + // Create API HTTP handler for the resource graph. + var api http.Handler api, err := rest.NewHandler(index) if err != nil { - log.Fatal().Err(err).Msg("Invalid API configuration") + log.Printf("E! Invalid API configuration: %s", err) + os.Exit(1) } - // Init an alice handler chain (use your preferred one) - c := alice.New() - - // Install a logger - c = c.Append(hlog.NewHandler(log.With().Logger())) - c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { - hlog.FromRequest(r).Info(). - Str("method", r.Method). - Str("url", r.URL.String()). - Int("status", status). - Int("size", size). - Dur("duration", duration). - Msg("") - })) - c = c.Append(hlog.RequestHandler("req")) - c = c.Append(hlog.RemoteAddrHandler("ip")) - c = c.Append(hlog.UserAgentHandler("ua")) - c = c.Append(hlog.RefererHandler("ref")) - c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id")) - resource.LoggerLevel = resource.LogLevelDebug - resource.Logger = func(ctx context.Context, level resource.LogLevel, msg string, fields map[string]interface{}) { - zerolog.Ctx(ctx).WithLevel(zerolog.Level(level)).Fields(fields).Msg(msg) - } - - // Log API access - c = c.Append(xaccess.NewHandler()) - // Add CORS support with passthrough option on so rest-layer can still - // handle OPTIONS method - c = c.Append(cors.New(cors.Options{OptionsPassthrough: true}).Handler) + // handle OPTIONS method. + api = cors.New(cors.Options{OptionsPassthrough: true}).Handler(api) + + // Wrap the api & cors handler with an access log middleware. + api = AccessLog(api) - // Bind the API under /api/ path - http.Handle("/api/", http.StripPrefix("/api/", c.Then(api))) + // Bind the API under the /api/ path. + http.Handle("/api/", http.StripPrefix("/api/", api)) - // Serve it - log.Info().Msg("Serving API on http://localhost:8080") + // Serve it. + log.Printf("I! Serving API on http://localhost:8080") if err := http.ListenAndServe(":8080", nil); err != nil { - log.Fatal().Err(err).Msg("") + log.Printf("E! Cannot serve API: %s", err) + os.Exit(1) } } diff --git a/examples/go.mod b/examples/go.mod new file mode 100644 index 00000000..e5cbac64 --- /dev/null +++ b/examples/go.mod @@ -0,0 +1,23 @@ +module github.com/rs/rest-layer/examples + +require ( + github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da + github.com/rs/cors v1.6.0 + github.com/rs/rest-layer v0.1.0 + github.com/rs/rest-layer-hystrix v0.0.0-20170801073253-2b89f63b98ec + github.com/rs/xaccess v0.0.0-20160803170743-f63036252bcc + github.com/rs/xhandler v0.0.0-20170707052532-1eb70cf1520d // indirect + github.com/rs/xlog v0.0.0-20171227185259-131980fab91b // indirect + github.com/rs/xstats v0.0.0-20170813190920-c67367528e16 // indirect + github.com/rs/zerolog v1.11.0 + github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect + github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect + github.com/zenazn/goji v0.9.0 // indirect + golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76 // indirect +) + +replace github.com/rs/rest-layer => ./../ diff --git a/examples/go.sum b/examples/go.sum new file mode 100644 index 00000000..1c405c37 --- /dev/null +++ b/examples/go.sum @@ -0,0 +1,46 @@ +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= +github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/graphql-go/graphql v0.7.6 h1:3Bn1IFB5OvPoANEfu03azF8aMyks0G/H6G1XeTfYbM4= +github.com/graphql-go/graphql v0.7.6/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A= +github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/rest-layer-hystrix v0.0.0-20170801073253-2b89f63b98ec h1:kZpeRs9xIb78yukn4OnXQmRk0OluJcuKSHWrsV4ekRw= +github.com/rs/rest-layer-hystrix v0.0.0-20170801073253-2b89f63b98ec/go.mod h1:OL5uSpiXaPH0ur9ni/u/CJi4MLxL3Hu1dK2h4jnF1y0= +github.com/rs/xaccess v0.0.0-20160803170743-f63036252bcc h1:8XPfocIg5yoqhb776Oo1c+on8F4FMtewE+LZzLLaCgQ= +github.com/rs/xaccess v0.0.0-20160803170743-f63036252bcc/go.mod h1:he5kXRmw8ajCYHpcXNno5IYxodyENhcVMp7FQd+qeHM= +github.com/rs/xhandler v0.0.0-20170707052532-1eb70cf1520d h1:8Tt7DYYdFqLlOIuyiE0RluKem4T+048AUafnIjH80wg= +github.com/rs/xhandler v0.0.0-20170707052532-1eb70cf1520d/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xlog v0.0.0-20171227185259-131980fab91b h1:65vbRzwfvVUk63GnEiBy1lsY40FLZQev13NK+LnyHAE= +github.com/rs/xlog v0.0.0-20171227185259-131980fab91b/go.mod h1:PJ0wmxt3GdhZAbIT0S8HQXsHuLt11tPiF8bUKXUV77w= +github.com/rs/xstats v0.0.0-20170813190920-c67367528e16 h1:m0aigb++JZXs+tzTO60LOOKSOXWyr7scDxlaSvU6HN8= +github.com/rs/xstats v0.0.0-20170813190920-c67367528e16/go.mod h1:5Cg6M3g+Dp4RSFNYBtjJxxjksZc00LbESra5Sz6fGSU= +github.com/rs/zerolog v1.11.0 h1:DRuq/S+4k52uJzBQciUcofXx45GrMC6yrEbb/CoK6+M= +github.com/rs/zerolog v1.11.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76 h1:xx5MUFyRQRbPk6VjWjIE1epE/K5AoDD8QUN116NCy8k= +golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..293c2343 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/rs/rest-layer + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/evanphx/json-patch v4.1.0+incompatible + github.com/graphql-go/graphql v0.7.6 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.6.0 + github.com/rs/xid v1.2.1 + github.com/stretchr/testify v1.2.2 + golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..c5d3e308 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= +github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/graphql-go/graphql v0.7.6 h1:3Bn1IFB5OvPoANEfu03azF8aMyks0G/H6G1XeTfYbM4= +github.com/graphql-go/graphql v0.7.6/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/vendor/github.com/rs/rest-layer-hystrix/hystrix.go b/vendor/github.com/rs/rest-layer-hystrix/hystrix.go deleted file mode 100644 index 05db91b1..00000000 --- a/vendor/github.com/rs/rest-layer-hystrix/hystrix.go +++ /dev/null @@ -1,148 +0,0 @@ -// Package restrix is a REST Layer resource storage wrapper to add hystrix support -// to the underlaying storage handler. -package restrix - -import ( - "context" - "fmt" - - "github.com/afex/hystrix-go/hystrix" - "github.com/rs/rest-layer/resource" - "github.com/rs/rest-layer/schema/query" -) - -type wrapper struct { - resource.Storer - getCmd string - findCmd string - countCmd string - insertCmd string - updateCmd string - deleteCmd string - clearCmd string -} - -type mgetWrapper struct { - wrapper - multiGetCmd string -} - -// Wrap wraps a REST Layer storage handler to add hystrix support to all -// handler's methods. -// -// Hystrix wraps each storage handlers into an hystrix command. One hystrix -// command is created per backend actions with the format .. -// -// Actions are Find, Insert, Update, Delete, Clear and MultiGet for handlers -// implementing MultiGetter interface. -// -// You must configure hystrix for each command you want to control and start the -// stream handler. -// See https://godoc.org/github.com/afex/hystrix-go/hystrix for more info and -// examples/hystrix/main.go for a usage example. -func Wrap(name string, h resource.Storer) resource.Storer { - w := wrapper{ - Storer: h, - getCmd: fmt.Sprintf("%s.Get", name), - findCmd: fmt.Sprintf("%s.Find", name), - countCmd: fmt.Sprintf("%s.Count", name), - insertCmd: fmt.Sprintf("%s.Insert", name), - updateCmd: fmt.Sprintf("%s.Update", name), - deleteCmd: fmt.Sprintf("%s.Delete", name), - clearCmd: fmt.Sprintf("%s.Clear", name), - } - if _, ok := h.(resource.MultiGetter); ok { - return mgetWrapper{ - wrapper: w, - multiGetCmd: fmt.Sprintf("%s.MultiGet", name), - } - } - return w -} - -func (w wrapper) Insert(ctx context.Context, items []*resource.Item) error { - return hystrix.Do(w.insertCmd, func() error { - return w.Storer.Insert(ctx, items) - }, nil) -} - -func (w wrapper) Update(ctx context.Context, item *resource.Item, original *resource.Item) error { - return hystrix.Do(w.updateCmd, func() error { - return w.Storer.Update(ctx, item, original) - }, nil) -} - -func (w wrapper) Delete(ctx context.Context, item *resource.Item) error { - return hystrix.Do(w.deleteCmd, func() error { - return w.Storer.Delete(ctx, item) - }, nil) -} - -func (w wrapper) Clear(ctx context.Context, q *query.Query) (deleted int, err error) { - out := make(chan int, 1) - errs := hystrix.Go(w.clearCmd, func() error { - deleted, err := w.Storer.Clear(ctx, q) - if err == nil { - out <- deleted - } - return err - }, nil) - select { - case deleted = <-out: - case err = <-errs: - } - return -} - -func (w wrapper) Find(ctx context.Context, q *query.Query) (list *resource.ItemList, err error) { - out := make(chan *resource.ItemList, 1) - errs := hystrix.Go(w.findCmd, func() error { - list, err := w.Storer.Find(ctx, q) - if err == nil { - out <- list - } - return err - }, nil) - select { - case list = <-out: - case err = <-errs: - } - return -} - -func (w wrapper) Count(ctx context.Context, q *query.Query) (total int, err error) { - c, ok := w.Storer.(resource.Counter) - if !ok { - return -1, resource.ErrNotImplemented - } - out := make(chan int, 1) - errs := hystrix.Go(w.countCmd, func() error { - total, err := c.Count(ctx, q) - if err == nil { - out <- total - } - return err - }, nil) - select { - case total = <-out: - case err = <-errs: - } - return -} - -func (w mgetWrapper) MultiGet(ctx context.Context, ids []interface{}) (items []*resource.Item, err error) { - out := make(chan []*resource.Item, 1) - errs := hystrix.Go(w.multiGetCmd, func() error { - mg := w.wrapper.Storer.(resource.MultiGetter) - items, err := mg.MultiGet(ctx, ids) - if err == nil { - out <- items - } - return err - }, nil) - select { - case items = <-out: - case err = <-errs: - } - return -}