package httpmock import ( "bytes" "context" "errors" "fmt" "net/http" "net/url" "regexp" "sort" "strconv" "strings" "sync" "github.com/jarcoal/httpmock/internal" ) const regexpPrefix = "=~" // NoResponderFound is returned when no responders are found for a // given HTTP method and URL. var NoResponderFound = internal.NoResponderFound var stdMethods = map[string]bool{ "CONNECT": true, // Section 9.9 "DELETE": true, // Section 9.7 "GET": true, // Section 9.3 "HEAD": true, // Section 9.4 "OPTIONS": true, // Section 9.2 "POST": true, // Section 9.5 "PUT": true, // Section 9.6 "TRACE": true, // Section 9.8 } // methodProbablyWrong returns true if method has probably wrong case. func methodProbablyWrong(method string) bool { return !stdMethods[method] && stdMethods[strings.ToUpper(method)] } // ConnectionFailure is a responder that returns a connection failure. // This is the default responder and is called when no other matching // responder is found. See [RegisterNoResponder] to override this // default behavior. func ConnectionFailure(*http.Request) (*http.Response, error) { return nil, NoResponderFound } // NewMockTransport creates a new [*MockTransport] with no responders. func NewMockTransport() *MockTransport { return &MockTransport{ responders: make(map[internal.RouteKey]matchResponders), callCountInfo: make(map[matchRouteKey]int), } } type regexpResponder struct { origRx string method string rx *regexp.Regexp responders matchResponders } // MockTransport implements [http.RoundTripper] interface, which // fulfills single HTTP requests issued by an [http.Client]. This // implementation doesn't actually make the call, instead deferring to // the registered list of responders. type MockTransport struct { // DontCheckMethod disables standard methods check. By default, if // a responder is registered using a lower-cased method among CONNECT, // DELETE, GET, HEAD, OPTIONS, POST, PUT and TRACE, a panic occurs // as it is probably a mistake. DontCheckMethod bool mu sync.RWMutex responders map[internal.RouteKey]matchResponders regexpResponders []regexpResponder noResponder Responder callCountInfo map[matchRouteKey]int totalCallCount int } var findForKey = []func(*MockTransport, internal.RouteKey) respondersFound{ (*MockTransport).respondersForKey, // Exact match (*MockTransport).regexpRespondersForKey, // Regexp match } type respondersFound struct { responders matchResponders key, respKey internal.RouteKey submatches []string } func (m *MockTransport) findResponders(method string, url *url.URL, fromIdx int) ( found respondersFound, findForKeyIndex int, ) { urlStr := url.String() key := internal.RouteKey{ Method: method, } for findForKeyIndex = fromIdx; findForKeyIndex <= len(findForKey)-1; findForKeyIndex++ { getResponders := findForKey[findForKeyIndex] // try and get a responder that matches the method and URL with // query params untouched: http://z.tld/path?q... key.URL = urlStr found = getResponders(m, key) if found.responders != nil { break } // if we weren't able to find some responders, try with the URL *and* // sorted query params query := sortedQuery(url.Query()) if query != "" { // Replace unsorted query params by sorted ones: // http://z.tld/path?sorted_q... key.URL = strings.Replace(urlStr, url.RawQuery, query, 1) found = getResponders(m, key) if found.responders != nil { break } } // if we weren't able to find some responders, try without any query params strippedURL := *url strippedURL.RawQuery = "" strippedURL.Fragment = "" // go1.6 does not handle URL.ForceQuery, so in case it is set in go>1.6, // remove the "?" manually if present. surl := strings.TrimSuffix(strippedURL.String(), "?") hasQueryString := urlStr != surl // if the URL contains a querystring then we strip off the // querystring and try again: http://z.tld/path if hasQueryString { key.URL = surl found = getResponders(m, key) if found.responders != nil { break } } // if we weren't able to find some responders for the full URL, try with // the path part only pathAlone := url.RawPath if pathAlone == "" { pathAlone = url.Path } // First with unsorted querystring: /path?q... if hasQueryString { key.URL = pathAlone + strings.TrimPrefix(urlStr, surl) // concat after-path part found = getResponders(m, key) if found.responders != nil { break } // Then with sorted querystring: /path?sorted_q... key.URL = pathAlone + "?" + sortedQuery(url.Query()) if url.Fragment != "" { key.URL += "#" + url.Fragment } found = getResponders(m, key) if found.responders != nil { break } } // Then using path alone: /path key.URL = pathAlone found = getResponders(m, key) if found.responders != nil { break } } found.key = key return } // suggestResponder is typically called after a findResponders failure // to suggest a user mistake. func (m *MockTransport) suggestResponder(method string, url *url.URL) *internal.ErrorNoResponderFoundMistake { // Responder not found, try to detect some common user mistakes on // method then on path var found respondersFound // On method first if methodProbablyWrong(method) { // Get → GET found, _ = m.findResponders(strings.ToUpper(method), url, 0) } if found.responders == nil { // Search for any other method found, _ = m.findResponders("", url, 0) } if found.responders != nil { return &internal.ErrorNoResponderFoundMistake{ Kind: "method", Orig: method, Suggested: found.respKey.Method, } } // Then on path if strings.HasSuffix(url.Path, "/") { // Try without final "/" u := *url u.Path = strings.TrimSuffix(u.Path, "/") found, _ = m.findResponders("", &u, 0) } if found.responders == nil && strings.Contains(url.Path, "//") { // Try without double "/" u := *url squash := false u.Path = strings.Map(func(r rune) rune { if r == '/' { if squash { return -1 } squash = true } else { squash = false } return r }, u.Path) found, _ = m.findResponders("", &u, 0) } if found.responders != nil { return &internal.ErrorNoResponderFoundMistake{ Kind: "URL", Orig: url.String(), Suggested: found.respKey.URL, } } return nil } // RoundTrip receives HTTP requests and routes them to the appropriate // responder. It is required to implement the [http.RoundTripper] // interface. You will not interact with this directly, instead the // [*http.Client] you are using will call it for you. func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { method := req.Method if method == "" { // http.Request.Method is documented to default to GET: method = http.MethodGet } var ( suggested *internal.ErrorNoResponderFoundMistake responder Responder fail bool found respondersFound findIdx int ) for fromFindIdx := 0; ; { found, findIdx = m.findResponders(method, req.URL, fromFindIdx) if found.responders == nil { if suggested == nil { // a suggestion is already available, no need of a new one suggested = m.suggestResponder(method, req.URL) fail = true } break } // we found some responders, check for one matcher mr := func() *matchResponder { m.mu.RLock() defer m.mu.RUnlock() return found.responders.findMatchResponder(req) }() if mr == nil { if suggested == nil { // a suggestion is not already available, do it now fail = true if len(found.responders) == 1 { suggested = &internal.ErrorNoResponderFoundMistake{ Kind: "matcher", Suggested: fmt.Sprintf("matcher %q", found.responders[0].matcher.name), } } else { names := make([]string, len(found.responders)) for i, mr := range found.responders { names[i] = mr.matcher.name } suggested = &internal.ErrorNoResponderFoundMistake{ Kind: "matcher", Suggested: fmt.Sprintf("%d matchers: %q", len(found.responders), names), } } } // No Matcher found for exact match, retry for regexp match if findIdx < len(findForKey)-1 { fromFindIdx = findIdx + 1 continue } break } // OK responder found fail = false responder = mr.responder m.mu.Lock() m.callCountInfo[matchRouteKey{RouteKey: found.key, name: mr.matcher.name}]++ if found.key != found.respKey { m.callCountInfo[matchRouteKey{RouteKey: found.respKey, name: mr.matcher.name}]++ } m.totalCallCount++ m.mu.Unlock() break } if fail { m.mu.Lock() if m.noResponder != nil { // we didn't find a responder, so fire the 'no responder' responder m.callCountInfo[matchRouteKey{RouteKey: internal.NoResponder}]++ m.totalCallCount++ // give a hint to NewNotFoundResponder() if it is a possible // method or URL error, or missing matcher if suggested != nil { req = req.WithContext(context.WithValue(req.Context(), suggestedKey, &suggestedInfo{ kind: suggested.Kind, suggested: suggested.Suggested, })) } responder = m.noResponder } m.mu.Unlock() } if responder == nil { if suggested != nil { return nil, suggested } return ConnectionFailure(req) } return runCancelable(responder, internal.SetSubmatches(req, found.submatches)) } func (m *MockTransport) numResponders() int { num := 0 for _, mrs := range m.responders { num += len(mrs) } for _, rr := range m.regexpResponders { num += len(rr.responders) } return num } // NumResponders returns the number of responders currently in use. // The responder registered with [MockTransport.RegisterNoResponder] // is not taken into account. func (m *MockTransport) NumResponders() int { m.mu.RLock() defer m.mu.RUnlock() return m.numResponders() } // Responders returns the list of currently registered responders. // Each responder is listed as a string containing "METHOD URL". // Non-regexp responders are listed first in alphabetical order // (sorted by URL then METHOD), then regexp responders in the order // they have been registered. // // The responder registered with [MockTransport.RegisterNoResponder] // is not listed. func (m *MockTransport) Responders() []string { m.mu.RLock() defer m.mu.RUnlock() rks := make([]internal.RouteKey, 0, len(m.responders)) for rk := range m.responders { rks = append(rks, rk) } sort.Slice(rks, func(i, j int) bool { if rks[i].URL == rks[j].URL { return rks[i].Method < rks[j].Method } return rks[i].URL < rks[j].URL }) rs := make([]string, 0, m.numResponders()) for _, rk := range rks { for _, mr := range m.responders[rk] { rs = append(rs, matchRouteKey{ RouteKey: rk, name: mr.matcher.name, }.String()) } } for _, rr := range m.regexpResponders { for _, mr := range rr.responders { rs = append(rs, matchRouteKey{ RouteKey: internal.RouteKey{ Method: rr.method, URL: rr.origRx, }, name: mr.matcher.name, }.String()) } } return rs } func runCancelable(responder Responder, req *http.Request) (*http.Response, error) { ctx := req.Context() if req.Cancel == nil && ctx.Done() == nil { // nolint: staticcheck resp, err := responder(req) return resp, internal.CheckStackTracer(req, err) } // Set up a goroutine that translates a close(req.Cancel) into a // "request canceled" error, and another one that runs the // responder. Then race them: first to the result channel wins. type result struct { response *http.Response err error } resultch := make(chan result, 1) done := make(chan struct{}, 1) go func() { select { case <-req.Cancel: // nolint: staticcheck resultch <- result{ response: nil, err: errors.New("request canceled"), } case <-ctx.Done(): resultch <- result{ response: nil, err: ctx.Err(), } case <-done: } }() go func() { defer func() { if err := recover(); err != nil { resultch <- result{ response: nil, err: fmt.Errorf("panic in responder: got %q", err), } } }() response, err := responder(req) resultch <- result{ response: response, err: err, } }() r := <-resultch // if a cancel() issued from context.WithCancel() or a // close(req.Cancel) are never coming, we'll need to unblock the // first goroutine. done <- struct{}{} return r.response, internal.CheckStackTracer(req, r.err) } // respondersForKey returns a responder for a given key. func (m *MockTransport) respondersForKey(key internal.RouteKey) respondersFound { m.mu.RLock() defer m.mu.RUnlock() if key.Method != "" { return respondersFound{ responders: m.responders[key], respKey: key, } } for k, resp := range m.responders { if key.URL == k.URL { return respondersFound{ responders: resp, respKey: k, } } } return respondersFound{} } // respondersForKeyUsingRegexp returns the first responder matching a // given key using regexps. func (m *MockTransport) regexpRespondersForKey(key internal.RouteKey) respondersFound { m.mu.RLock() defer m.mu.RUnlock() for _, regInfo := range m.regexpResponders { if key.Method == "" || regInfo.method == key.Method { if sm := regInfo.rx.FindStringSubmatch(key.URL); sm != nil { if len(sm) == 1 { sm = nil } else { sm = sm[1:] } return respondersFound{ responders: regInfo.responders, respKey: internal.RouteKey{ Method: regInfo.method, URL: regInfo.origRx, }, submatches: sm, } } } } return respondersFound{} } func isRegexpURL(url string) bool { return strings.HasPrefix(url, regexpPrefix) } func (m *MockTransport) checkMethod(method string, matcher Matcher) { if !m.DontCheckMethod && methodProbablyWrong(method) { panic(fmt.Sprintf("You probably want to use method %q instead of %q? If not and so want to disable this check, set MockTransport.DontCheckMethod field to true", strings.ToUpper(method), method, )) } } // RegisterMatcherResponder adds a new responder, associated with a given // HTTP method, URL (or path) and [Matcher]. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // If url contains query parameters, their order matters as well as // their content. All following URLs are here considered as different: // // http://z.tld?a=1&b=1 // http://z.tld?b=1&a=1 // http://z.tld?a&b // http://z.tld?a=&b= // // If url begins with "=~", the following chars are considered as a // regular expression. If this regexp can not be compiled, it panics. // Note that the "=~" prefix remains in statistics returned by // [MockTransport.GetCallCountInfo]. As 2 regexps can match the same // URL, the regexp responders are tested in the order they are // registered. Registering an already existing regexp responder (same // method & same regexp string) replaces its responder, but does not // change its position. // // Registering an already existing responder resets the corresponding // statistics as returned by [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // See [MockTransport.RegisterRegexpMatcherResponder] to directly pass a // [*regexp.Regexp]. // // If several responders are registered for a same method and url // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [MockTransport.RegisterResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See also [MockTransport.RegisterResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func (m *MockTransport) RegisterMatcherResponder(method, url string, matcher Matcher, responder Responder) { m.checkMethod(method, matcher) mr := matchResponder{ matcher: matcher, responder: responder, } if isRegexpURL(url) { rr := regexpResponder{ origRx: url, method: method, rx: regexp.MustCompile(url[2:]), responders: matchResponders{mr}, } m.registerRegexpResponder(rr) return } key := internal.RouteKey{ Method: method, URL: url, } m.mu.Lock() if responder == nil { if mrs := m.responders[key].remove(matcher.name); mrs == nil { delete(m.responders, key) } else { m.responders[key] = mrs } delete(m.callCountInfo, matchRouteKey{RouteKey: key, name: matcher.name}) } else { m.responders[key] = m.responders[key].add(mr) m.callCountInfo[matchRouteKey{RouteKey: key, name: matcher.name}] = 0 } m.mu.Unlock() } // RegisterResponder adds a new responder, associated with a given // HTTP method and URL (or path). // // When a request comes in that matches, the responder is called and // the response returned to the client. // // If url contains query parameters, their order matters as well as // their content. All following URLs are here considered as different: // // http://z.tld?a=1&b=1 // http://z.tld?b=1&a=1 // http://z.tld?a&b // http://z.tld?a=&b= // // If url begins with "=~", the following chars are considered as a // regular expression. If this regexp can not be compiled, it panics. // Note that the "=~" prefix remains in statistics returned by // [MockTransport.GetCallCountInfo]. As 2 regexps can match the same // URL, the regexp responders are tested in the order they are // registered. Registering an already existing regexp responder (same // method & same regexp string) replaces its responder, but does not // change its position. // // Registering an already existing responder resets the corresponding // statistics as returned by [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. // // See [MockTransport.RegisterRegexpResponder] to directly pass a // [*regexp.Regexp]. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See [MockTransport.RegisterMatcherResponder] to also match on // request header and/or body. func (m *MockTransport) RegisterResponder(method, url string, responder Responder) { m.RegisterMatcherResponder(method, url, Matcher{}, responder) } // It is the caller responsibility that len(rxResp.reponders) == 1. func (m *MockTransport) registerRegexpResponder(rxResp regexpResponder) { m.mu.Lock() defer m.mu.Unlock() mr := rxResp.responders[0] found: for { for i, rr := range m.regexpResponders { if rr.method == rxResp.method && rr.origRx == rxResp.origRx { if mr.responder == nil { rr.responders = rr.responders.remove(mr.matcher.name) if rr.responders == nil { copy(m.regexpResponders[:i], m.regexpResponders[i+1:]) m.regexpResponders[len(m.regexpResponders)-1] = regexpResponder{} m.regexpResponders = m.regexpResponders[:len(m.regexpResponders)-1] } else { m.regexpResponders[i] = rr } } else { rr.responders = rr.responders.add(mr) m.regexpResponders[i] = rr } break found } } if mr.responder != nil { m.regexpResponders = append(m.regexpResponders, rxResp) } break // nolint: staticcheck } mrk := matchRouteKey{ RouteKey: internal.RouteKey{ Method: rxResp.method, URL: rxResp.origRx, }, name: mr.matcher.name, } if mr.responder == nil { delete(m.callCountInfo, mrk) } else { m.callCountInfo[mrk] = 0 } } // RegisterRegexpMatcherResponder adds a new responder, associated // with a given HTTP method, URL (or path) regular expression and // [Matcher]. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // As 2 regexps can match the same URL, the regexp responders are // tested in the order they are registered. Registering an already // existing regexp responder (same method, same regexp string and same // [Matcher] name) replaces its responder, but does not change its // position, and resets the corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. // // If several responders are registered for a same method and urlRegexp // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [MockTransport.RegisterRegexpResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // A "=~" prefix is added to the stringified regexp in the statistics // returned by [MockTransport.GetCallCountInfo]. // // See [MockTransport.RegisterMatcherResponder] function and the "=~" // prefix in its url parameter to avoid compiling the regexp by // yourself. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See [MockTransport.RegisterRegexpResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func (m *MockTransport) RegisterRegexpMatcherResponder(method string, urlRegexp *regexp.Regexp, matcher Matcher, responder Responder) { m.checkMethod(method, matcher) m.registerRegexpResponder(regexpResponder{ origRx: regexpPrefix + urlRegexp.String(), method: method, rx: urlRegexp, responders: matchResponders{{matcher: matcher, responder: responder}}, }) } // RegisterRegexpResponder adds a new responder, associated with a given // HTTP method and URL (or path) regular expression. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // As 2 regexps can match the same URL, the regexp responders are // tested in the order they are registered. Registering an already // existing regexp responder (same method & same regexp string) // replaces its responder, but does not change its position, and // resets the corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.MockTransportGetCallCountInfo]. It does nothing if // it does not already exist. // // A "=~" prefix is added to the stringified regexp in the statistics // returned by [MockTransport.GetCallCountInfo]. // // See [MockTransport.RegisterResponder] function and the "=~" prefix // in its url parameter to avoid compiling the regexp by yourself. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See [MockTransport.RegisterRegexpMatcherResponder] to also match on // request header and/or body. func (m *MockTransport) RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) { m.RegisterRegexpMatcherResponder(method, urlRegexp, Matcher{}, responder) } // RegisterMatcherResponderWithQuery is same as // [MockTransport.RegisterMatcherResponder], but it doesn't depend on // query items order. // // If query is non-nil, its type can be: // // - [url.Values] // - map[string]string // - string, a query string like "a=12&a=13&b=z&c" (see [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [url.ParseQuery], a panic() occurs. // // Unlike [MockTransport.RegisterMatcherResponder], path cannot be // prefixed by "=~" to say it is a regexp. If it is, a panic occurs. // // Registering an already existing responder resets the corresponding // statistics as returned by [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // If several responders are registered for a same method, path and // query tuple, but with different matchers, they are ordered // depending on the following rules: // - the zero matcher, Matcher{} (or responder set using // [MockTransport.RegisterResponderWithQuery]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See also [MockTransport.RegisterResponderWithQuery] if a matcher is // not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func (m *MockTransport) RegisterMatcherResponderWithQuery(method, path string, query any, matcher Matcher, responder Responder) { if isRegexpURL(path) { panic(`path begins with "=~", RegisterResponder should be used instead of RegisterResponderWithQuery`) } var mapQuery url.Values switch q := query.(type) { case url.Values: mapQuery = q case map[string]string: mapQuery = make(url.Values, len(q)) for key, e := range q { mapQuery[key] = []string{e} } case string: var err error mapQuery, err = url.ParseQuery(q) if err != nil { panic("RegisterResponderWithQuery bad query string: " + err.Error()) } default: if query != nil { panic(fmt.Sprintf("RegisterResponderWithQuery bad query type %T. Only url.Values, map[string]string and string are allowed", query)) } } if queryString := sortedQuery(mapQuery); queryString != "" { path += "?" + queryString } m.RegisterMatcherResponder(method, path, matcher, responder) } // RegisterResponderWithQuery is same as // [MockTransport.RegisterResponder], but it doesn't depend on query // items order. // // If query is non-nil, its type can be: // // - [url.Values] // - map[string]string // - string, a query string like "a=12&a=13&b=z&c" (see [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [url.ParseQuery], a panic() occurs. // // Unlike [MockTransport.RegisterResponder], path cannot be prefixed // by "=~" to say it is a regexp. If it is, a panic occurs. // // Registering an already existing responder resets the corresponding // statistics as returned by [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See [MockTransport.RegisterMatcherResponderWithQuery] to also match on // request header and/or body. func (m *MockTransport) RegisterResponderWithQuery(method, path string, query any, responder Responder) { m.RegisterMatcherResponderWithQuery(method, path, query, Matcher{}, responder) } func sortedQuery(m url.Values) string { if len(m) == 0 { return "" } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) var b bytes.Buffer var values []string // nolint: prealloc for _, k := range keys { // Do not alter the passed url.Values values = append(values, m[k]...) sort.Strings(values) k = url.QueryEscape(k) for _, v := range values { if b.Len() > 0 { b.WriteByte('&') } fmt.Fprintf(&b, "%v=%v", k, url.QueryEscape(v)) } values = values[:0] } return b.String() } // RegisterNoResponder is used to register a responder that is called // if no other responders are found. The default is [ConnectionFailure] // that returns a connection error. // // Use it in conjunction with [NewNotFoundResponder] to ensure that all // routes have been mocked: // // func TestMyApp(t *testing.T) { // ... // // Calls testing.Fatal with the name of Responder-less route and // // the stack trace of the call. // mock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal)) // // will abort the current test and print something like: // // transport_test.go:735: Called from net/http.Get() // at /go/src/github.com/jarcoal/httpmock/transport_test.go:714 // github.com/jarcoal/httpmock.TestCheckStackTracer() // at /go/src/testing/testing.go:865 // testing.tRunner() // at /go/src/runtime/asm_amd64.s:1337 // // If responder is passed as nil, the default behavior // ([ConnectionFailure]) is re-enabled. // // In some cases you may not want all URLs to be mocked, in which case // you can do this: // // func TestFetchArticles(t *testing.T) { // ... // mock.RegisterNoResponder(httpmock.InitialTransport.RoundTrip) // // // any requests that don't have a registered URL will be fetched normally // } func (m *MockTransport) RegisterNoResponder(responder Responder) { m.mu.Lock() m.noResponder = responder m.mu.Unlock() } // Reset removes all registered responders (including the no // responder) from the [MockTransport]. It zeroes call counters too. func (m *MockTransport) Reset() { m.mu.Lock() m.responders = make(map[internal.RouteKey]matchResponders) m.regexpResponders = nil m.noResponder = nil m.callCountInfo = make(map[matchRouteKey]int) m.totalCallCount = 0 m.mu.Unlock() } // ZeroCallCounters zeroes call counters without touching registered responders. func (m *MockTransport) ZeroCallCounters() { m.mu.Lock() for k := range m.callCountInfo { m.callCountInfo[k] = 0 } m.totalCallCount = 0 m.mu.Unlock() } // GetCallCountInfo gets the info on all the calls m has caught // since it was activated or reset. The info is returned as a map of // the calling keys with the number of calls made to them as their // value. The key is the method, a space, and the URL all concatenated // together. // // As a special case, regexp responders generate 2 entries for each // call. One for the call caught and the other for the rule that // matched. For example: // // RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body")) // http.Get("http://z.com") // // will generate the following result: // // map[string]int{ // `GET http://z.com`: 1, // `GET =~z\.com\z`: 1, // } func (m *MockTransport) GetCallCountInfo() map[string]int { m.mu.RLock() res := make(map[string]int, len(m.callCountInfo)) for k, v := range m.callCountInfo { res[k.String()] = v } m.mu.RUnlock() return res } // GetTotalCallCount gets the total number of calls m has taken // since it was activated or reset. func (m *MockTransport) GetTotalCallCount() int { m.mu.RLock() defer m.mu.RUnlock() return m.totalCallCount } // DefaultTransport is the default mock transport used by [Activate], // [Deactivate], [Reset], [DeactivateAndReset], [RegisterResponder], // [RegisterRegexpResponder], [RegisterResponderWithQuery] and // [RegisterNoResponder]. var DefaultTransport = NewMockTransport() // InitialTransport is a cache of the original transport used so we // can put it back when [Deactivate] is called. var InitialTransport = http.DefaultTransport // oldClients is used to handle custom http clients (i.e clients other // than http.DefaultClient). var oldClients = map[*http.Client]http.RoundTripper{} // oldClientsLock protects oldClients from concurrent writes. var oldClientsLock sync.Mutex // Activate starts the mock environment. This should be called before // your tests run. Under the hood this replaces the [http.Client.Transport] // field of [http.DefaultClient] with [DefaultTransport]. // // To enable mocks for a test, simply activate at the beginning of a test: // // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // // all http requests using http.DefaultTransport will now be intercepted // } // // If you want all of your tests in a package to be mocked, just call // [Activate] from init(): // // func init() { // httpmock.Activate() // } // // or using a TestMain function: // // func TestMain(m *testing.M) { // httpmock.Activate() // os.Exit(m.Run()) // } func Activate() { if Disabled() { return } // make sure that if Activate is called multiple times it doesn't // overwrite the InitialTransport with a mock transport. if http.DefaultTransport != DefaultTransport { InitialTransport = http.DefaultTransport } http.DefaultTransport = DefaultTransport } // ActivateNonDefault starts the mock environment with a non-default // [*http.Client]. This emulates the [Activate] function, but allows for // custom clients that do not use [http.DefaultTransport]. // // To enable mocks for a test using a custom client, activate at the // beginning of a test: // // client := &http.Client{Transport: &http.Transport{TLSHandshakeTimeout: 60 * time.Second}} // httpmock.ActivateNonDefault(client) func ActivateNonDefault(client *http.Client) { if Disabled() { return } // save the custom client & it's RoundTripper oldClientsLock.Lock() defer oldClientsLock.Unlock() if _, ok := oldClients[client]; !ok { oldClients[client] = client.Transport } client.Transport = DefaultTransport } // GetCallCountInfo gets the info on all the calls httpmock has caught // since it was activated or reset. The info is returned as a map of // the calling keys with the number of calls made to them as their // value. The key is the method, a space, and the URL all concatenated // together. // // As a special case, regexp responders generate 2 entries for each // call. One for the call caught and the other for the rule that // matched. For example: // // RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body")) // http.Get("http://z.com") // // will generate the following result: // // map[string]int{ // `GET http://z.com`: 1, // `GET =~z\.com\z`: 1, // } func GetCallCountInfo() map[string]int { return DefaultTransport.GetCallCountInfo() } // GetTotalCallCount gets the total number of calls httpmock has taken // since it was activated or reset. func GetTotalCallCount() int { return DefaultTransport.GetTotalCallCount() } // Deactivate shuts down the mock environment. Any HTTP calls made // after this will use a live transport. // // Usually you'll call it in a defer right after activating the mock // environment: // // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.Deactivate() // // // when this test ends, the mock environment will close // } // // Since go 1.14 you can also use [*testing.T.Cleanup] method as in: // // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // t.Cleanup(httpmock.Deactivate) // // // when this test ends, the mock environment will close // } // // useful in test helpers to save your callers from calling defer themselves. func Deactivate() { if Disabled() { return } http.DefaultTransport = InitialTransport // reset the custom clients to use their original RoundTripper oldClientsLock.Lock() defer oldClientsLock.Unlock() for oldClient, oldTransport := range oldClients { oldClient.Transport = oldTransport delete(oldClients, oldClient) } } // Reset removes any registered mocks and returns the mock // environment to its initial state. It zeroes call counters too. func Reset() { DefaultTransport.Reset() } // ZeroCallCounters zeroes call counters without touching registered responders. func ZeroCallCounters() { DefaultTransport.ZeroCallCounters() } // DeactivateAndReset is just a convenience method for calling // [Deactivate] and then [Reset]. // // Happy deferring! func DeactivateAndReset() { Deactivate() Reset() } // RegisterMatcherResponder adds a new responder, associated with a given // HTTP method, URL (or path) and [Matcher]. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // If url contains query parameters, their order matters as well as // their content. All following URLs are here considered as different: // // http://z.tld?a=1&b=1 // http://z.tld?b=1&a=1 // http://z.tld?a&b // http://z.tld?a=&b= // // If url begins with "=~", the following chars are considered as a // regular expression. If this regexp can not be compiled, it panics. // Note that the "=~" prefix remains in statistics returned by // [GetCallCountInfo]. As 2 regexps can match the same // URL, the regexp responders are tested in the order they are // registered. Registering an already existing regexp responder (same // method & same regexp string) replaces its responder, but does not // change its position. // // Registering an already existing responder resets the corresponding // statistics as returned by [GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [GetCallCountInfo]. It does nothing if it does not // already exist. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // See [RegisterRegexpMatcherResponder] to directly pass a // [*regexp.Regexp]. // // Example: // // func TestCreateArticle(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // // Mock POST /item only if `"name":"Bob"` is found in request body // httpmock.RegisterMatcherResponder("POST", "/item", // httpmock.BodyContainsString(`"name":"Bob"`), // httpmock.NewStringResponder(201, `{"id":1234}`)) // // // Can be more acurate with github.com/maxatome/tdhttpmock package // // paired with github.com/maxatome/go-testdeep/td operators as in // httpmock.RegisterMatcherResponder("POST", "/item", // tdhttpmock.JSONBody(td.JSONPointer("/name", "Alice")), // httpmock.NewStringResponder(201, `{"id":4567}`)) // // // POST requests to http://anything/item with body containing either // // `"name":"Bob"` or a JSON message with key "name" set to "Alice" // // value return the corresponding "id" response // } // // If several responders are registered for a same method and url // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [RegisterResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See also [RegisterResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func RegisterMatcherResponder(method, url string, matcher Matcher, responder Responder) { DefaultTransport.RegisterMatcherResponder(method, url, matcher, responder) } // RegisterResponder adds a new responder, associated with a given // HTTP method and URL (or path). // // When a request comes in that matches, the responder is called and // the response returned to the client. // // If url contains query parameters, their order matters as well as // their content. All following URLs are here considered as different: // // http://z.tld?a=1&b=1 // http://z.tld?b=1&a=1 // http://z.tld?a&b // http://z.tld?a=&b= // // If url begins with "=~", the following chars are considered as a // regular expression. If this regexp can not be compiled, it panics. // Note that the "=~" prefix remains in statistics returned by // [GetCallCountInfo]. As 2 regexps can match the same URL, the regexp // responders are tested in the order they are registered. Registering // an already existing regexp responder (same method & same regexp // string) replaces its responder, but does not change its position. // // Registering an already existing responder resets the corresponding // statistics as returned by [GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by [GetCallCountInfo]. It does // nothing if it does not already exist. // // See [RegisterRegexpResponder] to directly pass a *regexp.Regexp. // // Example: // // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // httpmock.RegisterResponder("GET", "http://example.com/", // httpmock.NewStringResponder(200, "hello world")) // // httpmock.RegisterResponder("GET", "/path/only", // httpmock.NewStringResponder(200, "any host hello world")) // // httpmock.RegisterResponder("GET", `=~^/item/id/\d+\z`, // httpmock.NewStringResponder(200, "any item get")) // // // requests to http://example.com/ now return "hello world" and // // requests to any host with path /path/only return "any host hello world" // // requests to any host with path matching ^/item/id/\d+\z regular expression return "any item get" // } // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting // [DefaultTransport].DontCheckMethod to true prior to this call. func RegisterResponder(method, url string, responder Responder) { DefaultTransport.RegisterResponder(method, url, responder) } // RegisterRegexpMatcherResponder adds a new responder, associated // with a given HTTP method, URL (or path) regular expression and // [Matcher]. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // As 2 regexps can match the same URL, the regexp responders are // tested in the order they are registered. Registering an already // existing regexp responder (same method, same regexp string and same // [Matcher] name) replaces its responder, but does not change its // position, and resets the corresponding statistics as returned by // [GetCallCountInfo]. // // If several responders are registered for a same method and urlRegexp // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [RegisterRegexpResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by [GetCallCountInfo]. It does // nothing if it does not already exist. The original matcher can be // passed but also a new [Matcher] with the same name and a nil match // function as in: // // NewMatcher("original matcher name", nil) // // A "=~" prefix is added to the stringified regexp in the statistics // returned by [GetCallCountInfo]. // // See [RegisterMatcherResponder] function and the "=~" prefix in its // url parameter to avoid compiling the regexp by yourself. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See [RegisterRegexpResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func RegisterRegexpMatcherResponder(method string, urlRegexp *regexp.Regexp, matcher Matcher, responder Responder) { DefaultTransport.RegisterRegexpMatcherResponder(method, urlRegexp, matcher, responder) } // RegisterRegexpResponder adds a new responder, associated with a given // HTTP method and URL (or path) regular expression. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // As 2 regexps can match the same URL, the regexp responders are // tested in the order they are registered. Registering an already // existing regexp responder (same method & same regexp string) // replaces its responder, but does not change its position, and // resets the corresponding statistics as returned by [GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by [GetCallCountInfo]. It does // nothing if it does not already exist. // // A "=~" prefix is added to the stringified regexp in the statistics // returned by [GetCallCountInfo]. // // See [RegisterResponder] function and the "=~" prefix in its url // parameter to avoid compiling the regexp by yourself. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting // DefaultTransport.DontCheckMethod to true prior to this call. func RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) { DefaultTransport.RegisterRegexpResponder(method, urlRegexp, responder) } // RegisterMatcherResponderWithQuery is same as // [RegisterMatcherResponder], but it doesn't depend on query items // order. // // If query is non-nil, its type can be: // // - [url.Values] // - map[string]string // - string, a query string like "a=12&a=13&b=z&c" (see [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [url.ParseQuery], a panic() occurs. // // Unlike [RegisterMatcherResponder], path cannot be prefixed by "=~" // to say it is a regexp. If it is, a panic occurs. // // Registering an already existing responder resets the corresponding // statistics as returned by [GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by [GetCallCountInfo]. It does // nothing if it does not already exist. The original matcher can be // passed but also a new [Matcher] with the same name and a nil match // function as in: // // NewMatcher("original matcher name", nil) // // If several responders are registered for a same method, path and // query tuple, but with different matchers, they are ordered // depending on the following rules: // - the zero matcher, Matcher{} (or responder set using // [.RegisterResponderWithQuery]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. // // See also [RegisterResponderWithQuery] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func RegisterMatcherResponderWithQuery(method, path string, query any, matcher Matcher, responder Responder) { DefaultTransport.RegisterMatcherResponderWithQuery(method, path, query, matcher, responder) } // RegisterResponderWithQuery it is same as [RegisterResponder], but // doesn't depends on query items order. // // query type can be: // // - [url.Values] // - map[string]string // - string, a query string like "a=12&a=13&b=z&c" (see [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [url.ParseQuery], a panic() occurs. // // Unlike [RegisterResponder], path cannot be prefixed by "=~" to say it // is a regexp. If it is, a panic occurs. // // Registering an already existing responder resets the corresponding // statistics as returned by [GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by [GetCallCountInfo]. It does // nothing if it does not already exist. // // Example using a [url.Values]: // // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // expectedQuery := net.Values{ // "a": []string{"3", "1", "8"}, // "b": []string{"4", "2"}, // } // httpmock.RegisterResponderWithQueryValues( // "GET", "http://example.com/", expectedQuery, // httpmock.NewStringResponder(200, "hello world")) // // // requests to http://example.com?a=1&a=3&a=8&b=2&b=4 // // and to http://example.com?b=4&a=2&b=2&a=8&a=1 // // now return 'hello world' // } // // or using a map[string]string: // // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // expectedQuery := map[string]string{ // "a": "1", // "b": "2" // } // httpmock.RegisterResponderWithQuery( // "GET", "http://example.com/", expectedQuery, // httpmock.NewStringResponder(200, "hello world")) // // // requests to http://example.com?a=1&b=2 and http://example.com?b=2&a=1 now return 'hello world' // } // // or using a query string: // // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // expectedQuery := "a=3&b=4&b=2&a=1&a=8" // httpmock.RegisterResponderWithQueryValues( // "GET", "http://example.com/", expectedQuery, // httpmock.NewStringResponder(200, "hello world")) // // // requests to http://example.com?a=1&a=3&a=8&b=2&b=4 // // and to http://example.com?b=4&a=2&b=2&a=8&a=1 // // now return 'hello world' // } // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting // DefaultTransport.DontCheckMethod to true prior to this call. func RegisterResponderWithQuery(method, path string, query any, responder Responder) { RegisterMatcherResponderWithQuery(method, path, query, Matcher{}, responder) } // RegisterNoResponder is used to register a responder that is called // if no other responders are found. The default is [ConnectionFailure] // that returns a connection error. // // Use it in conjunction with [NewNotFoundResponder] to ensure that all // routes have been mocked: // // import ( // "testing" // "github.com/jarcoal/httpmock" // ) // ... // func TestMyApp(t *testing.T) { // ... // // Calls testing.Fatal with the name of Responder-less route and // // the stack trace of the call. // httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal)) // // will abort the current test and print something like: // // transport_test.go:735: Called from net/http.Get() // at /go/src/github.com/jarcoal/httpmock/transport_test.go:714 // github.com/jarcoal/httpmock.TestCheckStackTracer() // at /go/src/testing/testing.go:865 // testing.tRunner() // at /go/src/runtime/asm_amd64.s:1337 // // If responder is passed as nil, the default behavior // ([ConnectionFailure]) is re-enabled. // // In some cases you may not want all URLs to be mocked, in which case // you can do this: // // func TestFetchArticles(t *testing.T) { // ... // httpmock.RegisterNoResponder(httpmock.InitialTransport.RoundTrip) // // // any requests that don't have a registered URL will be fetched normally // } func RegisterNoResponder(responder Responder) { DefaultTransport.RegisterNoResponder(responder) } // ErrSubmatchNotFound is the error returned by GetSubmatch* functions // when the given submatch index cannot be found. var ErrSubmatchNotFound = errors.New("submatch not found") // GetSubmatch has to be used in Responders installed by // [RegisterRegexpResponder] or [RegisterResponder] + "=~" URL prefix // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). It allows to retrieve the n-th // submatch of the matching regexp, as a string. Example: // // RegisterResponder("GET", `=~^/item/name/([^/]+)\z`, // func(req *http.Request) (*http.Response, error) { // name, err := GetSubmatch(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]any{ // "id": 123, // "name": name, // }) // }) // // It panics if n < 1. See [MustGetSubmatch] to avoid testing the // returned error. func GetSubmatch(req *http.Request, n int) (string, error) { if n <= 0 { panic(fmt.Sprintf("getting submatches starts at 1, not %d", n)) } n-- submatches := internal.GetSubmatches(req) if n >= len(submatches) { return "", ErrSubmatchNotFound } return submatches[n], nil } // GetSubmatchAsInt has to be used in Responders installed by // [RegisterRegexpResponder] or [RegisterResponder] + "=~" URL prefix // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). It allows to retrieve the n-th // submatch of the matching regexp, as an int64. Example: // // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id, err := GetSubmatchAsInt(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]any{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. See [MustGetSubmatchAsInt] to avoid testing the // returned error. func GetSubmatchAsInt(req *http.Request, n int) (int64, error) { sm, err := GetSubmatch(req, n) if err != nil { return 0, err } return strconv.ParseInt(sm, 10, 64) } // GetSubmatchAsUint has to be used in Responders installed by // [RegisterRegexpResponder] or [RegisterResponder] + "=~" URL prefix // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). It allows to retrieve the n-th // submatch of the matching regexp, as a uint64. Example: // // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id, err := GetSubmatchAsUint(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]any{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. See [MustGetSubmatchAsUint] to avoid testing the // returned error. func GetSubmatchAsUint(req *http.Request, n int) (uint64, error) { sm, err := GetSubmatch(req, n) if err != nil { return 0, err } return strconv.ParseUint(sm, 10, 64) } // GetSubmatchAsFloat has to be used in Responders installed by // [RegisterRegexpResponder] or [RegisterResponder] + "=~" URL prefix // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). It allows to retrieve the n-th // submatch of the matching regexp, as a float64. Example: // // RegisterResponder("PATCH", `=~^/item/id/\d+\?height=(\d+(?:\.\d*)?)\z`, // func(req *http.Request) (*http.Response, error) { // height, err := GetSubmatchAsFloat(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]any{ // "id": id, // "name": "The beautiful name", // "height": height, // }) // }) // // It panics if n < 1. See [MustGetSubmatchAsFloat] to avoid testing the // returned error. func GetSubmatchAsFloat(req *http.Request, n int) (float64, error) { sm, err := GetSubmatch(req, n) if err != nil { return 0, err } return strconv.ParseFloat(sm, 64) } // MustGetSubmatch works as [GetSubmatch] except that it panics in // case of error (submatch not found). It has to be used in Responders // installed by [RegisterRegexpResponder] or [RegisterResponder] + // "=~" URL prefix (as well as [MockTransport.RegisterRegexpResponder] // or [MockTransport.RegisterResponder]). It allows to retrieve the // n-th submatch of the matching regexp, as a string. Example: // // RegisterResponder("GET", `=~^/item/name/([^/]+)\z`, // func(req *http.Request) (*http.Response, error) { // name := MustGetSubmatch(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]any{ // "id": 123, // "name": name, // }) // }) // // It panics if n < 1. func MustGetSubmatch(req *http.Request, n int) string { s, err := GetSubmatch(req, n) if err != nil { panic("GetSubmatch failed: " + err.Error()) } return s } // MustGetSubmatchAsInt works as [GetSubmatchAsInt] except that it // panics in case of error (submatch not found or invalid int64 // format). It has to be used in Responders installed by // [RegisterRegexpResponder] or [RegisterResponder] + "=~" URL prefix // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). It allows to retrieve the n-th // submatch of the matching regexp, as an int64. Example: // // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id := MustGetSubmatchAsInt(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]any{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. func MustGetSubmatchAsInt(req *http.Request, n int) int64 { i, err := GetSubmatchAsInt(req, n) if err != nil { panic("GetSubmatchAsInt failed: " + err.Error()) } return i } // MustGetSubmatchAsUint works as [GetSubmatchAsUint] except that it // panics in case of error (submatch not found or invalid uint64 // format). It has to be used in Responders installed by // [RegisterRegexpResponder] or [RegisterResponder] + "=~" URL prefix // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). It allows to retrieve the n-th // submatch of the matching regexp, as a uint64. Example: // // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id, err := MustGetSubmatchAsUint(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]any{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. func MustGetSubmatchAsUint(req *http.Request, n int) uint64 { u, err := GetSubmatchAsUint(req, n) if err != nil { panic("GetSubmatchAsUint failed: " + err.Error()) } return u } // MustGetSubmatchAsFloat works as [GetSubmatchAsFloat] except that it // panics in case of error (submatch not found or invalid float64 // format). It has to be used in Responders installed by // [RegisterRegexpResponder] or [RegisterResponder] + "=~" URL prefix // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). It allows to retrieve the n-th // submatch of the matching regexp, as a float64. Example: // // RegisterResponder("PATCH", `=~^/item/id/\d+\?height=(\d+(?:\.\d*)?)\z`, // func(req *http.Request) (*http.Response, error) { // height := MustGetSubmatchAsFloat(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]any{ // "id": id, // "name": "The beautiful name", // "height": height, // }) // }) // // It panics if n < 1. func MustGetSubmatchAsFloat(req *http.Request, n int) float64 { f, err := GetSubmatchAsFloat(req, n) if err != nil { panic("GetSubmatchAsFloat failed: " + err.Error()) } return f }