package httpexpect import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/url" "os" "reflect" "sort" "strings" "time" "github.com/ajg/form" "github.com/fatih/structs" "github.com/google/go-querystring/query" "github.com/gorilla/websocket" "github.com/imkira/go-interpol" ) // Request provides methods to incrementally build http.Request object, // send it, and receive response. type Request struct { config Config chain chain http *http.Request path string query url.Values form url.Values formbuf *bytes.Buffer multipart *multipart.Writer bodySetter string typeSetter string forceType bool wsUpgrade bool matchers []func(*Response) } // NewRequest returns a new Request object. // // method defines the HTTP method (GET, POST, PUT, etc.). path defines url path. // // Simple interpolation is allowed for {named} parameters in path: // - if pathargs is given, it's used to substitute first len(pathargs) parameters, // regardless of their names // - if WithPath() or WithPathObject() is called, it's used to substitute given // parameters by name // // For example: // req := NewRequest(config, "POST", "/repos/{user}/{repo}", "gavv", "httpexpect") // // path will be "/repos/gavv/httpexpect" // // Or: // req := NewRequest(config, "POST", "/repos/{user}/{repo}") // req.WithPath("user", "gavv") // req.WithPath("repo", "httpexpect") // // path will be "/repos/gavv/httpexpect" // // After interpolation, path is urlencoded and appended to Config.BaseURL, // separated by slash. If BaseURL ends with a slash and path (after interpolation) // starts with a slash, only single slash is inserted. func NewRequest(config Config, method, path string, pathargs ...interface{}) *Request { if config.RequestFactory == nil { panic("config.RequestFactory == nil") } if config.Client == nil { panic("config.Client == nil") } chain := makeChain(config.Reporter) n := 0 path, err := interpol.WithFunc(path, func(k string, w io.Writer) error { if n < len(pathargs) { if pathargs[n] == nil { chain.fail( "\nunexpected nil argument for url path format string:\n"+ " Request(\"%s\", %v...)", method, pathargs) } else { mustWrite(w, fmt.Sprint(pathargs[n])) } } else { mustWrite(w, "{") mustWrite(w, k) mustWrite(w, "}") } n++ return nil }) if err != nil { chain.fail(err.Error()) } hr, err := config.RequestFactory.NewRequest(method, config.BaseURL, nil) if err != nil { chain.fail(err.Error()) } return &Request{ config: config, chain: chain, path: path, http: hr, } } // WithMatcher attaches a matcher to the request. // All attached matchers are invoked in the Expect method for a newly // created Response. // // Example: // req := NewRequest(config, "GET", "/path") // req.WithMatcher(func (resp *httpexpect.Response) { // resp.Header("API-Version").NotEmpty() // }) func (r *Request) WithMatcher(matcher func(*Response)) *Request { r.matchers = append(r.matchers, matcher) return r } // WithClient sets client. // // The new client overwrites Config.Client. It will be used once to send the // request and receive a response. // // Example: // req := NewRequest(config, "GET", "/path") // req.WithClient(&http.Client{ // Transport: &http.Transport{ // DisableCompression: true, // }, // }) func (r *Request) WithClient(client Client) *Request { if r.chain.failed() { return r } if client == nil { r.chain.fail("\nunexpected nil client in WithClient") return r } r.config.Client = client return r } // WithHandler configures client to invoke the given handler directly. // // If Config.Client is http.Client, then only its Transport field is overwritten // because the client may contain some state shared among requests like a cookie // jar. Otherwise, the whole client is overwritten with a new client. // // Example: // req := NewRequest(config, "GET", "/path") // req.WithHandler(myServer.someHandler) func (r *Request) WithHandler(handler http.Handler) *Request { if r.chain.failed() { return r } if handler == nil { r.chain.fail("\nunexpected nil handler in WithHandler") return r } if client, ok := r.config.Client.(*http.Client); ok { client.Transport = NewBinder(handler) } else { r.config.Client = &http.Client{ Transport: NewBinder(handler), Jar: NewJar(), } } return r } // WithWebsocketUpgrade enables upgrades the connection to websocket. // // At least the following fields are added to the request header: // Upgrade: websocket // Connection: Upgrade // // The actual set of header fields is define by the protocol implementation // in the gorilla/websocket package. // // The user should then call the Response.Websocket() method which returns // the Websocket object. This object can be used to send messages to the // server, to inspect the received messages, and to close the websocket. // // Example: // req := NewRequest(config, "GET", "/path") // req.WithWebsocketUpgrade() // ws := req.Expect().Status(http.StatusSwitchingProtocols).Websocket() // defer ws.Disconnect() func (r *Request) WithWebsocketUpgrade() *Request { if r.chain.failed() { return r } r.wsUpgrade = true return r } // WithWebsocketDialer sets the custom websocket dialer. // // The new dialer overwrites Config.WebsocketDialer. It will be used once to establish // the WebSocket connection and receive a response of handshake result. // // Example: // req := NewRequest(config, "GET", "/path") // req.WithWebsocketUpgrade() // req.WithWebsocketDialer(&websocket.Dialer{ // EnableCompression: false, // }) // ws := req.Expect().Status(http.StatusSwitchingProtocols).Websocket() // defer ws.Disconnect() func (r *Request) WithWebsocketDialer(dialer WebsocketDialer) *Request { if r.chain.failed() { return r } if dialer == nil { r.chain.fail("\nunexpected nil dialer in WithWebsocketDialer") return r } r.config.WebsocketDialer = dialer return r } // WithPath substitutes named parameters in url path. // // value is converted to string using fmt.Sprint(). If there is no named // parameter '{key}' in url path, failure is reported. // // Named parameters are case-insensitive. // // Example: // req := NewRequest(config, "POST", "/repos/{user}/{repo}") // req.WithPath("user", "gavv") // req.WithPath("repo", "httpexpect") // // path will be "/repos/gavv/httpexpect" func (r *Request) WithPath(key string, value interface{}) *Request { if r.chain.failed() { return r } ok := false path, err := interpol.WithFunc(r.path, func(k string, w io.Writer) error { if strings.EqualFold(k, key) { if value == nil { r.chain.fail( "\nunexpected nil argument for url path format string:\n"+ " WithPath(\"%s\", %v)", key, value) } else { mustWrite(w, fmt.Sprint(value)) ok = true } } else { mustWrite(w, "{") mustWrite(w, k) mustWrite(w, "}") } return nil }) if err == nil { r.path = path } else { r.chain.fail(err.Error()) return r } if !ok { r.chain.fail("\nunexpected key for url path format string:\n"+ " WithPath(\"%s\", %v)\n\npath:\n %q", key, value, r.path) return r } return r } // WithPathObject substitutes multiple named parameters in url path. // // object should be map or struct. If object is struct, it's converted // to map using https://github.com/fatih/structs. Structs may contain // "path" struct tag, similar to "json" struct tag for json.Marshal(). // // Each map value is converted to string using fmt.Sprint(). If there // is no named parameter for some map '{key}' in url path, failure is // reported. // // Named parameters are case-insensitive. // // Example: // type MyPath struct { // Login string `path:"user"` // Repo string // } // // req := NewRequest(config, "POST", "/repos/{user}/{repo}") // req.WithPathObject(MyPath{"gavv", "httpexpect"}) // // path will be "/repos/gavv/httpexpect" // // req := NewRequest(config, "POST", "/repos/{user}/{repo}") // req.WithPathObject(map[string]string{"user": "gavv", "repo": "httpexpect"}) // // path will be "/repos/gavv/httpexpect" func (r *Request) WithPathObject(object interface{}) *Request { if r.chain.failed() { return r } if object == nil { return r } var ( m map[string]interface{} ok bool ) if reflect.Indirect(reflect.ValueOf(object)).Kind() == reflect.Struct { s := structs.New(object) s.TagName = "path" m = s.Map() } else { m, ok = canonMap(&r.chain, object) if !ok { return r } } for k, v := range m { r.WithPath(k, v) } return r } // WithQuery adds query parameter to request URL. // // value is converted to string using fmt.Sprint() and urlencoded. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithQuery("a", 123) // req.WithQuery("b", "foo") // // URL is now http://example.com/path?a=123&b=foo func (r *Request) WithQuery(key string, value interface{}) *Request { if r.chain.failed() { return r } if r.query == nil { r.query = make(url.Values) } r.query.Add(key, fmt.Sprint(value)) return r } // WithQueryObject adds multiple query parameters to request URL. // // object is converted to query string using github.com/google/go-querystring // if it's a struct or pointer to struct, or github.com/ajg/form otherwise. // // Various object types are supported. Structs may contain "url" struct tag, // similar to "json" struct tag for json.Marshal(). // // Example: // type MyURL struct { // A int `url:"a"` // B string `url:"b"` // } // // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithQueryObject(MyURL{A: 123, B: "foo"}) // // URL is now http://example.com/path?a=123&b=foo // // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithQueryObject(map[string]interface{}{"a": 123, "b": "foo"}) // // URL is now http://example.com/path?a=123&b=foo func (r *Request) WithQueryObject(object interface{}) *Request { if r.chain.failed() { return r } if object == nil { return r } var ( q url.Values err error ) if reflect.Indirect(reflect.ValueOf(object)).Kind() == reflect.Struct { q, err = query.Values(object) if err != nil { r.chain.fail(err.Error()) return r } } else { q, err = form.EncodeToValues(object) if err != nil { r.chain.fail(err.Error()) return r } } if r.query == nil { r.query = make(url.Values) } for k, v := range q { r.query[k] = append(r.query[k], v...) } return r } // WithQueryString parses given query string and adds it to request URL. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithQuery("a", 11) // req.WithQueryString("b=22&c=33") // // URL is now http://example.com/path?a=11&bb=22&c=33 func (r *Request) WithQueryString(query string) *Request { if r.chain.failed() { return r } v, err := url.ParseQuery(query) if err != nil { r.chain.fail(err.Error()) return r } if r.query == nil { r.query = make(url.Values) } for k, v := range v { r.query[k] = append(r.query[k], v...) } return r } // WithURL sets request URL. // // This URL overwrites Config.BaseURL. Request path passed to NewRequest() // is appended to this URL, separated by slash if necessary. // // Example: // req := NewRequest(config, "PUT", "/path") // req.WithURL("http://example.com") // // URL is now http://example.com/path func (r *Request) WithURL(urlStr string) *Request { if r.chain.failed() { return r } if u, err := url.Parse(urlStr); err == nil { r.http.URL = u } else { r.chain.fail(err.Error()) } return r } // WithHeaders adds given headers to request. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithHeaders(map[string]string{ // "Content-Type": "application/json", // }) func (r *Request) WithHeaders(headers map[string]string) *Request { if r.chain.failed() { return r } for k, v := range headers { r.WithHeader(k, v) } return r } // WithHeader adds given single header to request. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithHeader("Content-Type": "application/json") func (r *Request) WithHeader(k, v string) *Request { if r.chain.failed() { return r } switch http.CanonicalHeaderKey(k) { case "Host": r.http.Host = v case "Content-Type": if !r.forceType { delete(r.http.Header, "Content-Type") } r.forceType = true r.typeSetter = "WithHeader" r.http.Header.Add(k, v) default: r.http.Header.Add(k, v) } return r } // WithCookies adds given cookies to request. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithCookies(map[string]string{ // "foo": "aa", // "bar": "bb", // }) func (r *Request) WithCookies(cookies map[string]string) *Request { if r.chain.failed() { return r } for k, v := range cookies { r.WithCookie(k, v) } return r } // WithCookie adds given single cookie to request. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithCookie("name", "value") func (r *Request) WithCookie(k, v string) *Request { if r.chain.failed() { return r } r.http.AddCookie(&http.Cookie{ Name: k, Value: v, }) return r } // WithBasicAuth sets the request's Authorization header to use HTTP // Basic Authentication with the provided username and password. // // With HTTP Basic Authentication the provided username and password // are not encrypted. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithBasicAuth("john", "secret") func (r *Request) WithBasicAuth(username, password string) *Request { if r.chain.failed() { return r } r.http.SetBasicAuth(username, password) return r } // WithProto sets HTTP protocol version. // // proto should have form of "HTTP/{major}.{minor}", e.g. "HTTP/1.1". // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithProto("HTTP/2.0") func (r *Request) WithProto(proto string) *Request { if r.chain.failed() { return r } major, minor, ok := http.ParseHTTPVersion(proto) if !ok { r.chain.fail( "\nunexpected protocol version %q, expected \"HTTP/{major}.{minor}\"", proto) return r } r.http.ProtoMajor = major r.http.ProtoMinor = minor return r } // WithChunked enables chunked encoding and sets request body reader. // // Expect() will read all available data from given reader. Content-Length // is not set, and "chunked" Transfer-Encoding is used. // // If protocol version is not at least HTTP/1.1 (required for chunked // encoding), failure is reported. // // Example: // req := NewRequest(config, "PUT", "http://example.com/upload") // fh, _ := os.Open("data") // defer fh.Close() // req.WithHeader("Content-Type": "application/octet-stream") // req.WithChunked(fh) func (r *Request) WithChunked(reader io.Reader) *Request { if r.chain.failed() { return r } if !r.http.ProtoAtLeast(1, 1) { r.chain.fail("chunked Transfer-Encoding requires at least \"HTTP/1.1\","+ "but \"HTTP/%d.%d\" is enabled", r.http.ProtoMajor, r.http.ProtoMinor) return r } r.setBody("WithChunked", reader, -1, false) return r } // WithBytes sets request body to given slice of bytes. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithHeader("Content-Type": "application/json") // req.WithBytes([]byte(`{"foo": 123}`)) func (r *Request) WithBytes(b []byte) *Request { if r.chain.failed() { return r } if b == nil { r.setBody("WithBytes", nil, 0, false) } else { r.setBody("WithBytes", bytes.NewReader(b), len(b), false) } return r } // WithText sets Content-Type header to "text/plain; charset=utf-8" and // sets body to given string. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithText("hello, world!") func (r *Request) WithText(s string) *Request { if r.chain.failed() { return r } r.setType("WithText", "text/plain; charset=utf-8", false) r.setBody("WithText", strings.NewReader(s), len(s), false) return r } // WithJSON sets Content-Type header to "application/json; charset=utf-8" // and sets body to object, marshaled using json.Marshal(). // // Example: // type MyJSON struct { // Foo int `json:"foo"` // } // // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithJSON(MyJSON{Foo: 123}) // // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithJSON(map[string]interface{}{"foo": 123}) func (r *Request) WithJSON(object interface{}) *Request { if r.chain.failed() { return r } b, err := json.Marshal(object) if err != nil { r.chain.fail(err.Error()) return r } r.setType("WithJSON", "application/json; charset=utf-8", false) r.setBody("WithJSON", bytes.NewReader(b), len(b), false) return r } // WithForm sets Content-Type header to "application/x-www-form-urlencoded" // or (if WithMultipart() was called) "multipart/form-data", converts given // object to url.Values using github.com/ajg/form, and adds it to request body. // // Various object types are supported, including maps and structs. Structs may // contain "form" struct tag, similar to "json" struct tag for json.Marshal(). // See https://github.com/ajg/form for details. // // Multiple WithForm(), WithFormField(), and WithFile() calls may be combined. // If WithMultipart() is called, it should be called first. // // Example: // type MyForm struct { // Foo int `form:"foo"` // } // // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithForm(MyForm{Foo: 123}) // // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithForm(map[string]interface{}{"foo": 123}) func (r *Request) WithForm(object interface{}) *Request { if r.chain.failed() { return r } f, err := form.EncodeToValues(object) if err != nil { r.chain.fail(err.Error()) return r } if r.multipart != nil { r.setType("WithForm", "multipart/form-data", false) var keys []string for k := range f { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { if err := r.multipart.WriteField(k, f[k][0]); err != nil { r.chain.fail(err.Error()) return r } } } else { r.setType("WithForm", "application/x-www-form-urlencoded", false) if r.form == nil { r.form = make(url.Values) } for k, v := range f { r.form[k] = append(r.form[k], v...) } } return r } // WithFormField sets Content-Type header to "application/x-www-form-urlencoded" // or (if WithMultipart() was called) "multipart/form-data", converts given // value to string using fmt.Sprint(), and adds it to request body. // // Multiple WithForm(), WithFormField(), and WithFile() calls may be combined. // If WithMultipart() is called, it should be called first. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithFormField("foo", 123). // WithFormField("bar", 456) func (r *Request) WithFormField(key string, value interface{}) *Request { if r.chain.failed() { return r } if r.multipart != nil { r.setType("WithFormField", "multipart/form-data", false) err := r.multipart.WriteField(key, fmt.Sprint(value)) if err != nil { r.chain.fail(err.Error()) return r } } else { r.setType("WithFormField", "application/x-www-form-urlencoded", false) if r.form == nil { r.form = make(url.Values) } r.form[key] = append(r.form[key], fmt.Sprint(value)) } return r } // WithFile sets Content-Type header to "multipart/form-data", reads given // file and adds its contents to request body. // // If reader is given, it's used to read file contents. Otherwise, os.Open() // is used to read a file with given path. // // Multiple WithForm(), WithFormField(), and WithFile() calls may be combined. // WithMultipart() should be called before WithFile(), otherwise WithFile() // fails. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithFile("avatar", "./john.png") // // req := NewRequest(config, "PUT", "http://example.com/path") // fh, _ := os.Open("./john.png") // req.WithMultipart(). // WithFile("avatar", "john.png", fh) // fh.Close() func (r *Request) WithFile(key, path string, reader ...io.Reader) *Request { if r.chain.failed() { return r } r.setType("WithFile", "multipart/form-data", false) if r.multipart == nil { r.chain.fail("WithFile requires WithMultipart to be called first") return r } wr, err := r.multipart.CreateFormFile(key, path) if err != nil { r.chain.fail(err.Error()) return r } var rd io.Reader if len(reader) != 0 && reader[0] != nil { rd = reader[0] } else { f, err := os.Open(path) if err != nil { r.chain.fail(err.Error()) return r } rd = f defer f.Close() } if _, err := io.Copy(wr, rd); err != nil { r.chain.fail(err.Error()) return r } return r } // WithFileBytes is like WithFile, but uses given slice of bytes as the // file contents. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // fh, _ := os.Open("./john.png") // b, _ := ioutil.ReadAll(fh) // req.WithMultipart(). // WithFileBytes("avatar", "john.png", b) // fh.Close() func (r *Request) WithFileBytes(key, path string, data []byte) *Request { if r.chain.failed() { return r } return r.WithFile(key, path, bytes.NewReader(data)) } // WithMultipart sets Content-Type header to "multipart/form-data". // // After this call, WithForm() and WithFormField() switch to multipart // form instead of urlencoded form. // // If WithMultipart() is called, it should be called before WithForm(), // WithFormField(), and WithFile(). // // WithFile() always requires WithMultipart() to be called first. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithMultipart(). // WithForm(map[string]interface{}{"foo": 123}) func (r *Request) WithMultipart() *Request { if r.chain.failed() { return r } r.setType("WithMultipart", "multipart/form-data", false) if r.multipart == nil { r.formbuf = new(bytes.Buffer) r.multipart = multipart.NewWriter(r.formbuf) r.setBody("WithMultipart", r.formbuf, 0, false) } return r } // Expect constructs http.Request, sends it, receives http.Response, and // returns a new Response object to inspect received response. // // Request is sent using Config.Client interface, or Config.Dialer interface // in case of WebSocket request. // // Example: // req := NewRequest(config, "PUT", "http://example.com/path") // req.WithJSON(map[string]interface{}{"foo": 123}) // resp := req.Expect() // resp.Status(http.StatusOK) func (r *Request) Expect() *Response { resp := r.roundTrip() if resp == nil { return makeResponse(responseOpts{ config: r.config, chain: r.chain, }) } for _, matcher := range r.matchers { matcher(resp) } return resp } func (r *Request) roundTrip() *Response { if !r.encodeRequest() { return nil } if r.wsUpgrade { if !r.encodeWebsocketRequest() { return nil } } for _, printer := range r.config.Printers { printer.Request(r.http) } start := time.Now() var ( httpResp *http.Response websock *websocket.Conn ) if r.wsUpgrade { httpResp, websock = r.sendWebsocketRequest() } else { httpResp = r.sendRequest() } elapsed := time.Since(start) if httpResp == nil { return nil } for _, printer := range r.config.Printers { printer.Response(httpResp, elapsed) } return makeResponse(responseOpts{ config: r.config, chain: r.chain, response: httpResp, websocket: websock, rtt: &elapsed, }) } func (r *Request) encodeRequest() bool { if r.chain.failed() { return false } r.http.URL.Path = concatPaths(r.http.URL.Path, r.path) if r.query != nil { r.http.URL.RawQuery = r.query.Encode() } if r.multipart != nil { if err := r.multipart.Close(); err != nil { r.chain.fail(err.Error()) return false } r.setType("Expect", r.multipart.FormDataContentType(), true) r.setBody("Expect", r.formbuf, r.formbuf.Len(), true) } else if r.form != nil { s := r.form.Encode() r.setBody("WithForm or WithFormField", strings.NewReader(s), len(s), false) } return true } func (r *Request) encodeWebsocketRequest() bool { if r.chain.failed() { return false } if r.bodySetter != "" { r.chain.fail( "\nwebocket request can not have body:\n "+ "body set by %s\n webocket enabled by WithWebsocketUpgrade", r.bodySetter) return false } switch r.http.URL.Scheme { case "https": r.http.URL.Scheme = "wss" default: r.http.URL.Scheme = "ws" } return true } func (r *Request) sendRequest() *http.Response { if r.chain.failed() { return nil } resp, err := r.config.Client.Do(r.http) if err != nil { r.chain.fail(err.Error()) return nil } return resp } func (r *Request) sendWebsocketRequest() (*http.Response, *websocket.Conn) { if r.chain.failed() { return nil, nil } conn, resp, err := r.config.WebsocketDialer.Dial( r.http.URL.String(), r.http.Header) if err != nil && err != websocket.ErrBadHandshake { r.chain.fail(err.Error()) return nil, nil } return resp, conn } func (r *Request) setType(newSetter, newType string, overwrite bool) { if r.forceType { return } if !overwrite { previousType := r.http.Header.Get("Content-Type") if previousType != "" && previousType != newType { r.chain.fail( "\nambiguous request \"Content-Type\" header values:\n %q (set by %s)\n\n"+ "and:\n %q (wanted by %s)", previousType, r.typeSetter, newType, newSetter) return } } r.typeSetter = newSetter r.http.Header["Content-Type"] = []string{newType} } func (r *Request) setBody(setter string, reader io.Reader, len int, overwrite bool) { if !overwrite && r.bodySetter != "" { r.chain.fail( "\nambiguous request body contents:\n set by %s\n overwritten by %s", r.bodySetter, setter) return } if len > 0 && reader == nil { panic("invalid length") } if reader == nil { r.http.Body = nil r.http.ContentLength = 0 } else { r.http.Body = ioutil.NopCloser(reader) r.http.ContentLength = int64(len) } r.bodySetter = setter } func concatPaths(a, b string) string { if a == "" { return b } if b == "" { return a } a = strings.TrimSuffix(a, "/") b = strings.TrimPrefix(b, "/") return a + "/" + b } func mustWrite(w io.Writer, s string) { _, err := w.Write([]byte(s)) if err != nil { panic(err) } }