123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535 |
- package httpexpect
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "mime/multipart"
- "net"
- "net/http"
- "net/url"
- "os"
- "reflect"
- "sort"
- "strings"
- "sync"
- "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 {
- mu sync.Mutex
- config Config
- chain *chain
- redirectPolicy RedirectPolicy
- maxRedirects int
- retryPolicy RetryPolicy
- maxRetries int
- minRetryDelay time.Duration
- maxRetryDelay time.Duration
- sleepFn func(d time.Duration) <-chan time.Time
- timeout time.Duration
- httpReq *http.Request
- path string
- query url.Values
- form url.Values
- formbuf *bytes.Buffer
- multipart *multipart.Writer
- multipartFn func(w io.Writer) *multipart.Writer
- bodySetter string
- typeSetter string
- forceType bool
- expectCalled bool
- wsUpgrade bool
- transformers []func(*http.Request)
- matchers []func(*Response)
- }
- // Deprecated: use NewRequestC instead.
- func NewRequest(config Config, method, path string, pathargs ...interface{}) *Request {
- return NewRequestC(config, method, path, pathargs...)
- }
- // NewRequestC returns a new Request instance.
- //
- // Requirements for config are same as for WithConfig function.
- //
- // 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 := NewRequestC(config, "POST", "/repos/{user}/{repo}", "iris-contrib", "httpexpect")
- // // path will be "/repos/iris-contrib/httpexpect"
- //
- // Or:
- //
- // req := NewRequestC(config, "POST", "/repos/{user}/{repo}")
- // req.WithPath("user", "iris-contrib")
- // req.WithPath("repo", "httpexpect")
- // // path will be "/repos/iris-contrib/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 NewRequestC(config Config, method, path string, pathargs ...interface{}) *Request {
- config = config.withDefaults()
- return newRequest(
- newChainWithConfig(fmt.Sprintf("Request(%q)", method), config),
- config,
- method,
- path,
- pathargs...,
- )
- }
- func newRequest(
- parent *chain, config Config, method, path string, pathargs ...interface{},
- ) *Request {
- config.validate()
- r := &Request{
- config: config,
- chain: parent.clone(),
- redirectPolicy: defaultRedirectPolicy,
- maxRedirects: -1,
- retryPolicy: RetryTimeoutAndServerErrors,
- maxRetries: 0,
- minRetryDelay: time.Millisecond * 50,
- maxRetryDelay: time.Second * 5,
- sleepFn: func(d time.Duration) <-chan time.Time {
- return time.After(d)
- },
- multipartFn: func(w io.Writer) *multipart.Writer {
- return multipart.NewWriter(w)
- },
- }
- opChain := r.chain.enter("")
- defer opChain.leave()
- r.initPath(opChain, path, pathargs...)
- r.initReq(opChain, method)
- r.chain.setRequest(r)
- return r
- }
- func (r *Request) initPath(opChain *chain, path string, pathargs ...interface{}) {
- if len(pathargs) != 0 {
- var n int
- var err error
- path, err = interpol.WithFunc(path, func(k string, w io.Writer) error {
- if n < len(pathargs) {
- if pathargs[n] == nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{pathargs},
- Errors: []error{
- fmt.Errorf("unexpected nil argument at index %d", n),
- },
- })
- } else {
- mustWrite(w, fmt.Sprint(pathargs[n]))
- }
- } else {
- mustWrite(w, "{")
- mustWrite(w, k)
- mustWrite(w, "}")
- }
- n++
- return nil
- })
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{path},
- Errors: []error{
- errors.New("invalid interpol string"),
- err,
- },
- })
- }
- }
- r.path = path
- }
- func (r *Request) initReq(opChain *chain, method string) {
- httpReq, err := r.config.RequestFactory.NewRequest(method, r.config.BaseURL, nil)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- errors.New("failed to create http request"),
- err,
- },
- })
- }
- r.httpReq = httpReq
- }
- // Alias is similar to Value.Alias.
- func (r *Request) Alias(name string) *Request {
- opChain := r.chain.enter("Alias(%q)", name)
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- r.chain.setAlias(name)
- return r
- }
- // WithName sets convenient request name.
- // This name will be included in assertion reports for this request.
- // It does not affect assertion chain path, inlike Alias.
- //
- // Example:
- //
- // req := NewRequestC(config, "POST", "/api/login")
- // req.WithName("Login Request")
- func (r *Request) WithName(name string) *Request {
- opChain := r.chain.enter("WithName()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithName()") {
- return r
- }
- r.chain.setRequestName(name)
- return r
- }
- // WithReporter sets reporter to be used for this request.
- //
- // The new reporter overwrites AssertionHandler.
- // The new AssertionHandler is DefaultAssertionHandler with specified reporter,
- // existing Config.Formatter and nil Logger.
- // It will be used to report formatted fatal failure messages.
- //
- // Example:
- //
- // req := NewRequestC(config, "GET", "http://example.com/path")
- // req.WithReporter(t)
- func (r *Request) WithReporter(reporter Reporter) *Request {
- opChain := r.chain.enter("WithReporter()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithReporter()") {
- return r
- }
- if reporter == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- return r
- }
- handler := &DefaultAssertionHandler{
- Reporter: reporter,
- Formatter: r.config.Formatter,
- }
- r.chain.setHandler(handler)
- return r
- }
- // WithAssertionHandler sets assertion handler to be used for this request.
- //
- // The new handler overwrites assertion handler that will be used
- // by Request and its children (Response, Body, etc.).
- // It will be used to format and report test Failure or Success.
- //
- // Example:
- //
- // req := NewRequestC(config, "GET", "http://example.com/path")
- // req.WithAssertionHandler(&DefaultAssertionHandler{
- // Reporter: reporter,
- // Formatter: formatter,
- // })
- func (r *Request) WithAssertionHandler(handler AssertionHandler) *Request {
- opChain := r.chain.enter("WithAssertionHandler()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithAssertionHandler()") {
- return r
- }
- if handler == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- return r
- }
- r.chain.setHandler(handler)
- return r
- }
- // WithMatcher attaches a matcher to the request.
- // All attached matchers are invoked in the Expect method for a newly
- // created Response.
- //
- // Example:
- //
- // req := NewRequestC(config, "GET", "/path")
- // req.WithMatcher(func (resp *httpexpect.Response) {
- // resp.Header("API-Version").NotEmpty()
- // })
- func (r *Request) WithMatcher(matcher func(*Response)) *Request {
- opChain := r.chain.enter("WithMatcher()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithMatcher()") {
- return r
- }
- if matcher == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- return r
- }
- r.matchers = append(r.matchers, matcher)
- return r
- }
- // WithTransformer attaches a transform to the Request.
- // All attachhed transforms are invoked in the Expect methods for
- // http.Request struct, after it's encoded and before it's sent.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithTransformer(func(r *http.Request) { r.Header.Add("foo", "bar") })
- func (r *Request) WithTransformer(transformer func(*http.Request)) *Request {
- opChain := r.chain.enter("WithTransformer()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithTransformer()") {
- return r
- }
- if transformer == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- return r
- }
- r.transformers = append(r.transformers, transformer)
- 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 := NewRequestC(config, "GET", "/path")
- // req.WithClient(&http.Client{
- // Transport: &http.Transport{
- // DisableCompression: true,
- // },
- // })
- func (r *Request) WithClient(client Client) *Request {
- opChain := r.chain.enter("WithClient()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithClient()") {
- return r
- }
- if client == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- 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 := NewRequestC(config, "GET", "/path")
- // req.WithHandler(myServer.someHandler)
- func (r *Request) WithHandler(handler http.Handler) *Request {
- opChain := r.chain.enter("WithHandler()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithHandler()") {
- return r
- }
- if handler == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- return r
- }
- if client, ok := r.config.Client.(*http.Client); ok {
- clientCopy := *client
- clientCopy.Transport = NewBinder(handler)
- r.config.Client = &clientCopy
- } else {
- r.config.Client = &http.Client{
- Transport: NewBinder(handler),
- Jar: NewCookieJar(),
- }
- }
- return r
- }
- // WithContext sets the context.
- //
- // Config.Context will be overwritten.
- //
- // Any retries will stop after one is cancelled.
- // If the intended behavior is to continue any further retries, use WithTimeout.
- //
- // Example:
- //
- // ctx, _ = context.WithTimeout(context.Background(), time.Duration(3)*time.Second)
- // req := NewRequestC(config, "GET", "/path")
- // req.WithContext(ctx)
- // req.Expect().Status(http.StatusOK)
- func (r *Request) WithContext(ctx context.Context) *Request {
- opChain := r.chain.enter("WithContext()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithContext()") {
- return r
- }
- if ctx == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- return r
- }
- r.config.Context = ctx
- return r
- }
- // WithTimeout sets a timeout duration for the request.
- //
- // Will attach to the request a context.WithTimeout around the Config.Context
- // or any context set WithContext. If these are nil, the new context will be
- // created on top of a context.Background().
- //
- // Any retries will continue after one is cancelled.
- // If the intended behavior is to stop any further retries, use WithContext or
- // Config.Context.
- //
- // Example:
- //
- // req := NewRequestC(config, "GET", "/path")
- // req.WithTimeout(time.Duration(3)*time.Second)
- // req.Expect().Status(http.StatusOK)
- func (r *Request) WithTimeout(timeout time.Duration) *Request {
- opChain := r.chain.enter("WithTimeout()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithTimeout()") {
- return r
- }
- r.timeout = timeout
- return r
- }
- // RedirectPolicy defines how redirection responses are handled.
- //
- // Status codes 307, 308 require resending body. They are followed only if
- // redirect policy is FollowAllRedirects.
- //
- // Status codes 301, 302, 303 don't require resending body. On such redirect,
- // http.Client automatically switches HTTP method to GET, if it's not GET or
- // HEAD already. These redirects are followed if redirect policy is either
- // FollowAllRedirects or FollowRedirectsWithoutBody.
- //
- // Default redirect policy is FollowRedirectsWithoutBody.
- type RedirectPolicy int
- const (
- // indicates that WithRedirectPolicy was not called
- defaultRedirectPolicy RedirectPolicy = iota
- // DontFollowRedirects forbids following any redirects.
- // Redirection response is returned to the user and can be inspected.
- DontFollowRedirects
- // FollowAllRedirects allows following any redirects, including those
- // which require resending body.
- FollowAllRedirects
- // FollowRedirectsWithoutBody allows following only redirects which
- // don't require resending body.
- // If redirect requires resending body, it's not followed, and redirection
- // response is returned instead.
- FollowRedirectsWithoutBody
- )
- // WithRedirectPolicy sets policy for redirection response handling.
- //
- // How redirect is handled depends on both response status code and
- // redirect policy. See comments for RedirectPolicy for details.
- //
- // Default redirect policy is defined by Client implementation.
- // Default behavior of http.Client corresponds to FollowRedirectsWithoutBody.
- //
- // This method can be used only if Client interface points to
- // *http.Client struct, since we rely on it in redirect handling.
- //
- // Example:
- //
- // req1 := NewRequestC(config, "POST", "/path")
- // req1.WithRedirectPolicy(FollowAllRedirects)
- // req1.Expect().Status(http.StatusOK)
- //
- // req2 := NewRequestC(config, "POST", "/path")
- // req2.WithRedirectPolicy(DontFollowRedirects)
- // req2.Expect().Status(http.StatusPermanentRedirect)
- func (r *Request) WithRedirectPolicy(policy RedirectPolicy) *Request {
- opChain := r.chain.enter("WithRedirectPolicy()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithRedirectPolicy()") {
- return r
- }
- r.redirectPolicy = policy
- return r
- }
- // WithMaxRedirects sets maximum number of redirects to follow.
- //
- // If the number of redirects exceedes this limit, request is failed.
- //
- // Default limit is defined by Client implementation.
- // Default behavior of http.Client corresponds to maximum of 10-1 redirects.
- //
- // This method can be used only if Client interface points to
- // *http.Client struct, since we rely on it in redirect handling.
- //
- // Example:
- //
- // req1 := NewRequestC(config, "POST", "/path")
- // req1.WithMaxRedirects(1)
- // req1.Expect().Status(http.StatusOK)
- func (r *Request) WithMaxRedirects(maxRedirects int) *Request {
- opChain := r.chain.enter("WithMaxRedirects()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithMaxRedirects()") {
- return r
- }
- if maxRedirects < 0 {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{maxRedirects},
- Errors: []error{
- errors.New("invalid negative argument"),
- },
- })
- return r
- }
- r.maxRedirects = maxRedirects
- return r
- }
- // RetryPolicy defines how failed requests are retried.
- //
- // Whether a request is retried depends on error type (if any), response
- // status code (if any), and retry policy.
- type RetryPolicy int
- const (
- // DontRetry disables retrying at all.
- DontRetry RetryPolicy = iota
- // Deprecated: use RetryTimeoutErrors instead.
- RetryTemporaryNetworkErrors
- // Deprecated: use RetryTimeoutAndServerErrors instead.
- RetryTemporaryNetworkAndServerErrors
- // RetryTimeoutErrors enables retrying of timeout errors.
- // Retry happens if Client returns net.Error and its Timeout() method
- // returns true.
- RetryTimeoutErrors
- // RetryTimeoutAndServerErrors enables retrying of network timeout errors,
- // as well as 5xx status codes.
- RetryTimeoutAndServerErrors
- // RetryAllErrors enables retrying of any error or 4xx/5xx status code.
- RetryAllErrors
- )
- // WithRetryPolicy sets policy for retries.
- //
- // Whether a request is retried depends on error type (if any), response
- // status code (if any), and retry policy.
- //
- // How much retry attempts happens is defined by WithMaxRetries().
- // How much to wait between attempts is defined by WithRetryDelay().
- //
- // Default retry policy is RetryTimeoutAndServerErrors, but
- // default maximum number of retries is zero, so no retries happen
- // unless WithMaxRetries() is called.
- //
- // Example:
- //
- // req := NewRequestC(config, "POST", "/path")
- // req.WithRetryPolicy(RetryAllErrors)
- // req.Expect().Status(http.StatusOK)
- func (r *Request) WithRetryPolicy(policy RetryPolicy) *Request {
- opChain := r.chain.enter("WithRetryPolicy()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithRetryPolicy()") {
- return r
- }
- r.retryPolicy = policy
- return r
- }
- // WithMaxRetries sets maximum number of retry attempts.
- //
- // After first request failure, additional retry attempts may happen,
- // depending on the retry policy.
- //
- // Setting this to zero disables retries, i.e. only one request is sent.
- // Setting this to N enables retries, and up to N+1 requests may be sent.
- //
- // Default number of retries is zero, i.e. retries are disabled.
- //
- // Example:
- //
- // req := NewRequestC(config, "POST", "/path")
- // req.WithMaxRetries(1)
- // req.Expect().Status(http.StatusOK)
- func (r *Request) WithMaxRetries(maxRetries int) *Request {
- opChain := r.chain.enter("WithMaxRetries()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithMaxRetries()") {
- return r
- }
- if maxRetries < 0 {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{maxRetries},
- Errors: []error{
- errors.New("invalid negative argument"),
- },
- })
- return r
- }
- r.maxRetries = maxRetries
- return r
- }
- // WithRetryDelay sets minimum and maximum delay between retries.
- //
- // If multiple retry attempts happen, delay between attempts starts from
- // minDelay and then grows exponentionally until it reaches maxDelay.
- //
- // Default delay range is [50ms; 5s].
- //
- // Example:
- //
- // req := NewRequestC(config, "POST", "/path")
- // req.WithRetryDelay(time.Second, time.Minute)
- // req.Expect().Status(http.StatusOK)
- func (r *Request) WithRetryDelay(minDelay, maxDelay time.Duration) *Request {
- opChain := r.chain.enter("WithRetryDelay()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithRetryDelay()") {
- return r
- }
- if !(minDelay <= maxDelay) {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{
- [2]time.Duration{minDelay, maxDelay},
- },
- Errors: []error{
- errors.New("invalid delay range"),
- },
- })
- return r
- }
- r.minRetryDelay = minDelay
- r.maxRetryDelay = maxDelay
- 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 instance. This instance can be used to send messages to the
- // server, to inspect the received messages, and to close the websocket.
- //
- // Example:
- //
- // req := NewRequestC(config, "GET", "/path")
- // req.WithWebsocketUpgrade()
- // ws := req.Expect().Status(http.StatusSwitchingProtocols).Websocket()
- // defer ws.Disconnect()
- func (r *Request) WithWebsocketUpgrade() *Request {
- opChain := r.chain.enter("WithWebsocketUpgrade()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithWebsocketUpgrade()") {
- 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 := NewRequestC(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 {
- opChain := r.chain.enter("WithWebsocketDialer()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithWebsocketDialer()") {
- return r
- }
- if dialer == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- 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 := NewRequestC(config, "POST", "/repos/{user}/{repo}")
- // req.WithPath("user", "iris-contrib")
- // req.WithPath("repo", "httpexpect")
- // // path will be "/repos/iris-contrib/httpexpect"
- func (r *Request) WithPath(key string, value interface{}) *Request {
- opChain := r.chain.enter("WithPath()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithPath()") {
- return r
- }
- r.withPath(opChain, key, value)
- 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 := NewRequestC(config, "POST", "/repos/{user}/{repo}")
- // req.WithPathObject(MyPath{"iris-contrib", "httpexpect"})
- // // path will be "/repos/iris-contrib/httpexpect"
- //
- // req := NewRequestC(config, "POST", "/repos/{user}/{repo}")
- // req.WithPathObject(map[string]string{"user": "iris-contrib", "repo": "httpexpect"})
- // // path will be "/repos/iris-contrib/httpexpect"
- func (r *Request) WithPathObject(object interface{}) *Request {
- opChain := r.chain.enter("WithPathObject()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithPathObject()") {
- 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(opChain, object)
- if !ok {
- return r
- }
- }
- for key, value := range m {
- r.withPath(opChain, key, value)
- }
- return r
- }
- func (r *Request) withPath(opChain *chain, key string, value interface{}) {
- found := false
- path, err := interpol.WithFunc(r.path, func(k string, w io.Writer) error {
- if strings.EqualFold(k, key) {
- if value == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf("unexpected nil interpol argument %q", k),
- },
- })
- } else {
- mustWrite(w, fmt.Sprint(value))
- found = true
- }
- } else {
- mustWrite(w, "{")
- mustWrite(w, k)
- mustWrite(w, "}")
- }
- return nil
- })
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{path},
- Errors: []error{
- errors.New("invalid interpol string"),
- err,
- },
- })
- return
- }
- if !found {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf("key %q not found in interpol string", key),
- },
- })
- return
- }
- r.path = path
- }
- // WithQuery adds query parameter to request URL.
- //
- // value is converted to string using fmt.Sprint() and urlencoded.
- //
- // Example:
- //
- // req := NewRequestC(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 {
- opChain := r.chain.enter("WithQuery()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithQuery()") {
- return r
- }
- if value == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected nil argument"),
- },
- })
- 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 := NewRequestC(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 := NewRequestC(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 {
- opChain := r.chain.enter("WithQueryObject()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithQueryObject()") {
- 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 {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{object},
- Errors: []error{
- errors.New("invalid query object"),
- err,
- },
- })
- return r
- }
- } else {
- q, err = form.EncodeToValues(object)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{object},
- Errors: []error{
- errors.New("invalid query object"),
- err,
- },
- })
- 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 := NewRequestC(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 {
- opChain := r.chain.enter("WithQueryString()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithQueryString()") {
- return r
- }
- v, err := url.ParseQuery(query)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{query},
- Errors: []error{
- errors.New("invalid query string"),
- err,
- },
- })
- 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 request constructor
- // is appended to this URL, separated by slash if necessary.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "/path")
- // req.WithURL("http://example.com")
- // // URL is now http://example.com/path
- func (r *Request) WithURL(urlStr string) *Request {
- opChain := r.chain.enter("WithURL()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithURL()") {
- return r
- }
- u, err := url.Parse(urlStr)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{urlStr},
- Errors: []error{
- errors.New("invalid url string"),
- err,
- },
- })
- return r
- }
- r.httpReq.URL = u
- return r
- }
- // WithHeaders adds given headers to request.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithHeaders(map[string]string{
- // "Content-Type": "application/json",
- // })
- func (r *Request) WithHeaders(headers map[string]string) *Request {
- opChain := r.chain.enter("WithHeaders()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithHeaders()") {
- return r
- }
- for k, v := range headers {
- r.withHeader(k, v)
- }
- return r
- }
- // WithHeader adds given single header to request.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithHeader("Content-Type", "application/json")
- func (r *Request) WithHeader(k, v string) *Request {
- opChain := r.chain.enter("WithHeader()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithHeader()") {
- return r
- }
- r.withHeader(k, v)
- return r
- }
- func (r *Request) withHeader(k, v string) {
- switch http.CanonicalHeaderKey(k) {
- case "Host":
- r.httpReq.Host = v
- case "Content-Type":
- if !r.forceType {
- delete(r.httpReq.Header, "Content-Type")
- }
- r.forceType = true
- r.typeSetter = "WithHeader()"
- r.httpReq.Header.Add(k, v)
- default:
- r.httpReq.Header.Add(k, v)
- }
- }
- // WithCookies adds given cookies to request.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithCookies(map[string]string{
- // "foo": "aa",
- // "bar": "bb",
- // })
- func (r *Request) WithCookies(cookies map[string]string) *Request {
- opChain := r.chain.enter("WithCookies()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithCookies()") {
- return r
- }
- for k, v := range cookies {
- r.httpReq.AddCookie(&http.Cookie{
- Name: k,
- Value: v,
- })
- }
- return r
- }
- // WithCookie adds given single cookie to request.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithCookie("name", "value")
- func (r *Request) WithCookie(k, v string) *Request {
- opChain := r.chain.enter("WithCookie()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithCookie()") {
- return r
- }
- r.httpReq.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 := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithBasicAuth("john", "secret")
- func (r *Request) WithBasicAuth(username, password string) *Request {
- opChain := r.chain.enter("WithBasicAuth()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithBasicAuth()") {
- return r
- }
- r.httpReq.SetBasicAuth(username, password)
- return r
- }
- // WithHost sets request host to given string.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithHost("example.com")
- func (r *Request) WithHost(host string) *Request {
- opChain := r.chain.enter("WithHost()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithHost()") {
- return r
- }
- r.httpReq.Host = host
- return r
- }
- // WithProto sets HTTP protocol version.
- //
- // proto should have form of "HTTP/{major}.{minor}", e.g. "HTTP/1.1".
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithProto("HTTP/2.0")
- func (r *Request) WithProto(proto string) *Request {
- opChain := r.chain.enter("WithProto()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithProto()") {
- return r
- }
- major, minor, ok := http.ParseHTTPVersion(proto)
- if !ok {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf(
- `unexpected protocol version %q, expected "HTTP/{major}.{minor}"`,
- proto),
- },
- })
- return r
- }
- r.httpReq.ProtoMajor = major
- r.httpReq.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 := NewRequestC(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 {
- opChain := r.chain.enter("WithChunked()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithChunked()") {
- return r
- }
- if !r.httpReq.ProtoAtLeast(1, 1) {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf(
- `chunked Transfer-Encoding requires at least "HTTP/1.1",`+
- ` but "HTTP/%d.%d" is used`,
- r.httpReq.ProtoMajor, r.httpReq.ProtoMinor),
- },
- })
- return r
- }
- r.setBody(opChain, "WithChunked()", reader, -1, false)
- return r
- }
- // WithBytes sets request body to given slice of bytes.
- //
- // Example:
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithHeader("Content-Type", "application/json")
- // req.WithBytes([]byte(`{"foo": 123}`))
- func (r *Request) WithBytes(b []byte) *Request {
- opChain := r.chain.enter("WithBytes()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithBytes()") {
- return r
- }
- if b == nil {
- r.setBody(opChain, "WithBytes()", nil, 0, false)
- } else {
- r.setBody(opChain, "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 := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithText("hello, world!")
- func (r *Request) WithText(s string) *Request {
- opChain := r.chain.enter("WithText()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithText()") {
- return r
- }
- r.setType(opChain, "WithText()", "text/plain; charset=utf-8", false)
- r.setBody(opChain, "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 := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithJSON(MyJSON{Foo: 123})
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithJSON(map[string]interface{}{"foo": 123})
- func (r *Request) WithJSON(object interface{}) *Request {
- opChain := r.chain.enter("WithJSON()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithJSON()") {
- return r
- }
- b, err := json.Marshal(object)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{object},
- Errors: []error{
- errors.New("invalid json object"),
- err,
- },
- })
- return r
- }
- r.setType(opChain, "WithJSON()", "application/json; charset=utf-8", false)
- r.setBody(opChain, "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 := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithForm(MyForm{Foo: 123})
- //
- // req := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithForm(map[string]interface{}{"foo": 123})
- func (r *Request) WithForm(object interface{}) *Request {
- opChain := r.chain.enter("WithForm()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithForm()") {
- return r
- }
- f, err := form.EncodeToValues(object)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertValid,
- Actual: &AssertionValue{object},
- Errors: []error{
- errors.New("invalid form object"),
- err,
- },
- })
- return r
- }
- if r.multipart != nil {
- r.setType(opChain, "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 {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- fmt.Errorf("failed to write multipart form field %q", k),
- err,
- },
- })
- return r
- }
- }
- } else {
- r.setType(opChain, "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 := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithFormField("foo", 123).
- // WithFormField("bar", 456)
- func (r *Request) WithFormField(key string, value interface{}) *Request {
- opChain := r.chain.enter("WithFormField()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithFormField()") {
- return r
- }
- if r.multipart != nil {
- r.setType(opChain, "WithFormField()", "multipart/form-data", false)
- err := r.multipart.WriteField(key, fmt.Sprint(value))
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- fmt.Errorf("failed to write multipart form field %q", key),
- err,
- },
- })
- return r
- }
- } else {
- r.setType(opChain, "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 := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithFile("avatar", "./john.png")
- //
- // req := NewRequestC(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 {
- opChain := r.chain.enter("WithFile()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithFile()") {
- return r
- }
- if len(reader) > 1 {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New("unexpected multiple reader arguments"),
- },
- })
- return r
- }
- r.withFile(opChain, "WithFile()", key, path, reader...)
- return r
- }
- // WithFileBytes is like WithFile, but uses given slice of bytes as the
- // file contents.
- //
- // Example:
- //
- // req := NewRequestC(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 {
- opChain := r.chain.enter("WithFileBytes()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithFileBytes()") {
- return r
- }
- r.withFile(opChain, "WithFileBytes()", key, path, bytes.NewReader(data))
- return r
- }
- func (r *Request) withFile(
- opChain *chain, method, key, path string, reader ...io.Reader,
- ) {
- r.setType(opChain, method, "multipart/form-data", false)
- if r.multipart == nil {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf("%s requires WithMultipart() to be called first", method),
- },
- })
- return
- }
- wr, err := r.multipart.CreateFormFile(key, path)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- fmt.Errorf(
- "failed to create form file with key %q and path %q",
- key, path),
- err,
- },
- })
- return
- }
- var rd io.Reader
- if len(reader) != 0 && reader[0] != nil {
- rd = reader[0]
- } else {
- f, err := os.Open(path)
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- fmt.Errorf("failed to open file %q", path),
- err,
- },
- })
- return
- }
- rd = f
- defer f.Close()
- }
- if _, err := io.Copy(wr, rd); err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- fmt.Errorf("failed to read file %q", path),
- err,
- },
- })
- return
- }
- }
- // 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 := NewRequestC(config, "PUT", "http://example.com/path")
- // req.WithMultipart().
- // WithForm(map[string]interface{}{"foo": 123})
- func (r *Request) WithMultipart() *Request {
- opChain := r.chain.enter("WithMultipart()")
- defer opChain.leave()
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return r
- }
- if !r.checkOrder(opChain, "WithMultipart()") {
- return r
- }
- r.setType(opChain, "WithMultipart()", "multipart/form-data", false)
- if r.multipart == nil {
- r.formbuf = &bytes.Buffer{}
- r.multipart = r.multipartFn(r.formbuf)
- r.setBody(opChain, "WithMultipart()", r.formbuf, 0, false)
- }
- return r
- }
- // Expect constructs http.Request, sends it, receives http.Response, and
- // returns a new Response instance.
- //
- // Request is sent using Client interface, or WebsocketDialer in case of
- // WebSocket request.
- //
- // After calling Expect, there should not be any more calls of Expect or
- // other WithXXX methods on the same Request instance.
- //
- // Example:
- //
- // req := NewRequestC(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 {
- opChain := r.chain.enter("Expect()")
- defer opChain.leave()
- resp := r.expect(opChain)
- if resp == nil {
- resp = newResponse(responseOpts{
- config: r.config,
- chain: opChain,
- })
- }
- return resp
- }
- func (r *Request) expect(opChain *chain) *Response {
- if !r.prepare(opChain) {
- return nil
- }
- // after return from prepare(), all subsequent calls to WithXXX and Expect will
- // abort early due to checkOrder(); so we can safely proceed without a lock
- resp := r.execute(opChain)
- if resp == nil {
- return nil
- }
- for _, matcher := range r.matchers {
- matcher(resp)
- }
- return resp
- }
- func (r *Request) prepare(opChain *chain) bool {
- r.mu.Lock()
- defer r.mu.Unlock()
- if opChain.failed() {
- return false
- }
- if !r.checkOrder(opChain, "Expect()") {
- return false
- }
- r.expectCalled = true
- return true
- }
- func (r *Request) execute(opChain *chain) *Response {
- if !r.encodeRequest(opChain) {
- return nil
- }
- if r.wsUpgrade {
- if !r.encodeWebsocketRequest(opChain) {
- return nil
- }
- }
- for _, transform := range r.transformers {
- transform(r.httpReq)
- if opChain.failed() {
- return nil
- }
- }
- var (
- httpResp *http.Response
- websock *websocket.Conn
- elapsed time.Duration
- )
- if r.wsUpgrade {
- httpResp, websock, elapsed = r.sendWebsocketRequest(opChain)
- } else {
- httpResp, elapsed = r.sendRequest(opChain)
- }
- if httpResp == nil {
- return nil
- }
- return newResponse(responseOpts{
- config: r.config,
- chain: opChain,
- httpResp: httpResp,
- websocket: websock,
- rtt: []time.Duration{elapsed},
- })
- }
- func (r *Request) encodeRequest(opChain *chain) bool {
- r.httpReq.URL.Path = concatPaths(r.httpReq.URL.Path, r.path)
- if r.query != nil {
- r.httpReq.URL.RawQuery = r.query.Encode()
- }
- if r.multipart != nil {
- if err := r.multipart.Close(); err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- errors.New("failed to close multipart form"),
- err,
- },
- })
- return false
- }
- r.setType(opChain, "Expect()", r.multipart.FormDataContentType(), true)
- r.setBody(opChain, "Expect()", r.formbuf, r.formbuf.Len(), true)
- } else if r.form != nil {
- s := r.form.Encode()
- r.setBody(opChain,
- "WithForm() or WithFormField()", strings.NewReader(s), len(s), false)
- }
- if r.httpReq.Body == nil {
- r.httpReq.Body = http.NoBody
- }
- if r.config.Context != nil {
- r.httpReq = r.httpReq.WithContext(r.config.Context)
- }
- r.setupRedirects(opChain)
- return true
- }
- var websocketErr = `webocket request can not have body:
- body was set by %s
- webocket was enabled by WithWebsocketUpgrade()`
- func (r *Request) encodeWebsocketRequest(opChain *chain) bool {
- if r.bodySetter != "" {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf(websocketErr, r.bodySetter),
- },
- })
- return false
- }
- switch r.httpReq.URL.Scheme {
- case "https":
- r.httpReq.URL.Scheme = "wss"
- default:
- r.httpReq.URL.Scheme = "ws"
- }
- return true
- }
- func (r *Request) sendRequest(opChain *chain) (*http.Response, time.Duration) {
- resp, elapsed, err := r.retryRequest(func() (*http.Response, error) {
- return r.config.Client.Do(r.httpReq)
- })
- if err != nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- errors.New("failed to send http request"),
- err,
- },
- })
- return nil, 0
- }
- return resp, elapsed
- }
- func (r *Request) sendWebsocketRequest(opChain *chain) (
- *http.Response, *websocket.Conn, time.Duration,
- ) {
- var conn *websocket.Conn
- resp, elapsed, err := r.retryRequest(func() (resp *http.Response, err error) {
- conn, resp, err = r.config.WebsocketDialer.Dial(
- r.httpReq.URL.String(), r.httpReq.Header)
- return resp, err
- })
- if err != nil && err != websocket.ErrBadHandshake {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- errors.New("failed to send websocket request"),
- err,
- },
- })
- return nil, nil, 0
- }
- if conn == nil {
- opChain.fail(AssertionFailure{
- Type: AssertOperation,
- Errors: []error{
- errors.New("failed to upgrade connection to websocket"),
- },
- })
- return nil, nil, 0
- }
- return resp, conn, elapsed
- }
- func (r *Request) retryRequest(reqFunc func() (*http.Response, error)) (
- *http.Response, time.Duration, error,
- ) {
- if r.httpReq.Body != nil && r.httpReq.Body != http.NoBody {
- if _, ok := r.httpReq.Body.(*bodyWrapper); !ok {
- r.httpReq.Body = newBodyWrapper(r.httpReq.Body, nil)
- }
- }
- reqBody, _ := r.httpReq.Body.(*bodyWrapper)
- delay := r.minRetryDelay
- i := 0
- for {
- for _, printer := range r.config.Printers {
- if reqBody != nil {
- reqBody.Rewind()
- }
- printer.Request(r.httpReq)
- }
- if reqBody != nil {
- reqBody.Rewind()
- }
- var cancelFn context.CancelFunc
- if r.timeout > 0 {
- var ctx context.Context
- if r.config.Context != nil {
- ctx, cancelFn = context.WithTimeout(r.config.Context, r.timeout)
- } else {
- ctx, cancelFn = context.WithTimeout(context.Background(), r.timeout)
- }
- r.httpReq = r.httpReq.WithContext(ctx)
- }
- start := time.Now()
- resp, err := reqFunc()
- elapsed := time.Since(start)
- if resp != nil && resp.Body != nil {
- resp.Body = newBodyWrapper(resp.Body, cancelFn)
- } else if cancelFn != nil {
- cancelFn()
- }
- if resp != nil {
- for _, printer := range r.config.Printers {
- if resp.Body != nil {
- resp.Body.(*bodyWrapper).Rewind()
- }
- printer.Response(resp, elapsed)
- }
- }
- i++
- if i == r.maxRetries+1 {
- return resp, elapsed, err
- }
- if !r.shouldRetry(resp, err) {
- return resp, elapsed, err
- }
- if resp != nil && resp.Body != nil {
- resp.Body.Close()
- }
- if configCtx := r.config.Context; configCtx != nil {
- select {
- case <-configCtx.Done():
- return nil, elapsed, configCtx.Err()
- case <-r.sleepFn(delay):
- }
- } else {
- <-r.sleepFn(delay)
- }
- delay *= 2
- if delay > r.maxRetryDelay {
- delay = r.maxRetryDelay
- }
- }
- }
- func (r *Request) shouldRetry(resp *http.Response, err error) bool {
- var (
- isTemporaryNetworkError bool // Deprecated
- isTimeoutError bool
- isServerError bool
- isHTTPError bool
- )
- if netErr, ok := err.(net.Error); ok {
- //nolint
- isTemporaryNetworkError = netErr.Temporary()
- isTimeoutError = netErr.Timeout()
- }
- if resp != nil {
- isServerError = resp.StatusCode >= 500 && resp.StatusCode <= 599
- isHTTPError = resp.StatusCode >= 400 && resp.StatusCode <= 599
- }
- switch r.retryPolicy {
- case DontRetry:
- break
- case RetryTemporaryNetworkErrors:
- return isTemporaryNetworkError
- case RetryTemporaryNetworkAndServerErrors:
- return isTemporaryNetworkError || isServerError
- case RetryTimeoutErrors:
- return isTimeoutError
- case RetryTimeoutAndServerErrors:
- return isTimeoutError || isServerError
- case RetryAllErrors:
- return err != nil || isHTTPError
- }
- return false
- }
- func (r *Request) setupRedirects(opChain *chain) {
- httpClient, _ := r.config.Client.(*http.Client)
- if httpClient == nil {
- if r.redirectPolicy != defaultRedirectPolicy {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New(
- "WithRedirectPolicy() can be used only if Client is *http.Client"),
- },
- })
- return
- }
- if r.maxRedirects != -1 {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- errors.New(
- "WithMaxRedirects() can be used only if Client is *http.Client"),
- },
- })
- return
- }
- } else {
- if r.redirectPolicy != defaultRedirectPolicy || r.maxRedirects != -1 {
- clientCopy := *httpClient
- httpClient = &clientCopy
- r.config.Client = &clientCopy
- }
- }
- if r.redirectPolicy == DontFollowRedirects {
- httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
- }
- } else if r.maxRedirects >= 0 {
- httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- if len(via) > r.maxRedirects {
- return fmt.Errorf("stopped after %d redirects", r.maxRedirects)
- }
- return nil
- }
- } else if r.redirectPolicy != defaultRedirectPolicy {
- httpClient.CheckRedirect = nil
- }
- if r.redirectPolicy == FollowAllRedirects {
- if r.httpReq.Body != nil && r.httpReq.Body != http.NoBody {
- if _, ok := r.httpReq.Body.(*bodyWrapper); !ok {
- r.httpReq.Body = newBodyWrapper(r.httpReq.Body, nil)
- }
- r.httpReq.GetBody = r.httpReq.Body.(*bodyWrapper).GetBody
- } else {
- r.httpReq.GetBody = func() (io.ReadCloser, error) {
- return http.NoBody, nil
- }
- }
- } else if r.redirectPolicy != defaultRedirectPolicy {
- r.httpReq.GetBody = nil
- }
- }
- var typeErr = `ambiguous request "Content-Type" header values:
- first set by %s:
- %q
- then replaced by %s:
- %q`
- func (r *Request) setType(
- opChain *chain, newSetter, newType string, overwrite bool,
- ) {
- if r.forceType {
- return
- }
- if !overwrite {
- previousType := r.httpReq.Header.Get("Content-Type")
- if previousType != "" && previousType != newType {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf(typeErr,
- r.typeSetter, previousType, newSetter, newType),
- },
- })
- return
- }
- }
- r.typeSetter = newSetter
- r.httpReq.Header["Content-Type"] = []string{newType}
- }
- var bodyErr = `ambiguous request body contents:
- first set by %s
- then replaced by %s`
- func (r *Request) setBody(
- opChain *chain, setter string, reader io.Reader, len int, overwrite bool,
- ) {
- if !overwrite && r.bodySetter != "" {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf(bodyErr, r.bodySetter, setter),
- },
- })
- return
- }
- if len > 0 && reader == nil {
- panic("invalid length")
- }
- if reader == nil {
- r.httpReq.Body = http.NoBody
- r.httpReq.ContentLength = 0
- } else {
- r.httpReq.Body = ioutil.NopCloser(reader)
- r.httpReq.ContentLength = int64(len)
- }
- r.bodySetter = setter
- }
- func (r *Request) checkOrder(opChain *chain, funcCall string) bool {
- if r.expectCalled {
- opChain.fail(AssertionFailure{
- Type: AssertUsage,
- Errors: []error{
- fmt.Errorf("unexpected call to %s: Expect() has already been called", funcCall),
- },
- })
- return false
- }
- return true
- }
- 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)
- }
- }
|