1 package httpmock 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "encoding/xml" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "reflect" 13 "strconv" 14 "strings" 15 "sync" 16 "time" 17 18 "github.com/jarcoal/httpmock/internal" 19 ) 20 21 // fromThenKeyType is used by Then(). 22 type fromThenKeyType struct{} 23 24 var fromThenKey = fromThenKeyType{} 25 26 type suggestedInfo struct { 27 kind string 28 suggested string 29 } 30 31 // suggestedMethodKeyType is used by NewNotFoundResponder(). 32 type suggestedKeyType struct{} 33 34 var suggestedKey = suggestedKeyType{} 35 36 // Responder is a callback that receives an http request and returns 37 // a mocked response. 38 type Responder func(*http.Request) (*http.Response, error) 39 40 func (r Responder) times(name string, n int, fn ...func(...any)) Responder { 41 count := 0 42 return func(req *http.Request) (*http.Response, error) { 43 count++ 44 if count > n { 45 err := internal.StackTracer{ 46 Err: fmt.Errorf("Responder not found for %s %s (coz %s and already called %d times)", req.Method, req.URL, name, count), 47 } 48 if len(fn) > 0 { 49 err.CustomFn = fn[0] 50 } 51 return nil, err 52 } 53 return r(req) 54 } 55 } 56 57 // Times returns a Responder callable n times before returning an 58 // error. If the Responder is called more than n times and fn is 59 // passed and non-nil, it acts as the fn parameter of 60 // NewNotFoundResponder, allowing to dump the stack trace to localize 61 // the origin of the call. 62 // import ( 63 // "testing" 64 // "github.com/jarcoal/httpmock" 65 // ) 66 // ... 67 // func TestMyApp(t *testing.T) { 68 // ... 69 // // This responder is callable 3 times, then an error is returned and 70 // // the stacktrace of the call logged using t.Log() 71 // httpmock.RegisterResponder("GET", "/foo/bar", 72 // httpmock.NewStringResponder(200, "{}").Times(3, t.Log), 73 // ) 74 func (r Responder) Times(n int, fn ...func(...any)) Responder { 75 return r.times("Times", n, fn...) 76 } 77 78 // Once returns a new Responder callable once before returning an 79 // error. If the Responder is called 2 or more times and fn is passed 80 // and non-nil, it acts as the fn parameter of NewNotFoundResponder, 81 // allowing to dump the stack trace to localize the origin of the 82 // call. 83 // import ( 84 // "testing" 85 // "github.com/jarcoal/httpmock" 86 // ) 87 // ... 88 // func TestMyApp(t *testing.T) { 89 // ... 90 // // This responder is callable only once, then an error is returned and 91 // // the stacktrace of the call logged using t.Log() 92 // httpmock.RegisterResponder("GET", "/foo/bar", 93 // httpmock.NewStringResponder(200, "{}").Once(t.Log), 94 // ) 95 func (r Responder) Once(fn ...func(...any)) Responder { 96 return r.times("Once", 1, fn...) 97 } 98 99 // Trace returns a new Responder that allows to easily trace the calls 100 // of the original Responder using fn. It can be used in conjunction 101 // with the testing package as in the example below with the help of 102 // (*testing.T).Log method: 103 // import ( 104 // "testing" 105 // "github.com/jarcoal/httpmock" 106 // ) 107 // ... 108 // func TestMyApp(t *testing.T) { 109 // ... 110 // httpmock.RegisterResponder("GET", "/foo/bar", 111 // httpmock.NewStringResponder(200, "{}").Trace(t.Log), 112 // ) 113 func (r Responder) Trace(fn func(...any)) Responder { 114 return func(req *http.Request) (*http.Response, error) { 115 resp, err := r(req) 116 return resp, internal.StackTracer{ 117 CustomFn: fn, 118 Err: err, 119 } 120 } 121 } 122 123 // Delay returns a new Responder that calls the original r Responder 124 // after a delay of d. 125 // import ( 126 // "testing" 127 // "time" 128 // "github.com/jarcoal/httpmock" 129 // ) 130 // ... 131 // func TestMyApp(t *testing.T) { 132 // ... 133 // httpmock.RegisterResponder("GET", "/foo/bar", 134 // httpmock.NewStringResponder(200, "{}").Delay(100*time.Millisecond), 135 // ) 136 func (r Responder) Delay(d time.Duration) Responder { 137 return func(req *http.Request) (*http.Response, error) { 138 time.Sleep(d) 139 return r(req) 140 } 141 } 142 143 var errThenDone = errors.New("ThenDone") 144 145 // similar is simple but a bit tricky. Here we consider two Responder 146 // are similar if they share the same function, but not necessarily 147 // the same environment. It is only used by Then below. 148 func (r Responder) similar(other Responder) bool { 149 return reflect.ValueOf(r).Pointer() == reflect.ValueOf(other).Pointer() 150 } 151 152 // Then returns a new Responder that calls r on first invocation, then 153 // next on following ones, except when Then is chained, in this case 154 // next is called only once: 155 // A := httpmock.NewStringResponder(200, "A") 156 // B := httpmock.NewStringResponder(200, "B") 157 // C := httpmock.NewStringResponder(200, "C") 158 // 159 // httpmock.RegisterResponder("GET", "/pipo", A.Then(B).Then(C)) 160 // 161 // http.Get("http://foo.bar/pipo") // A is called 162 // http.Get("http://foo.bar/pipo") // B is called 163 // http.Get("http://foo.bar/pipo") // C is called 164 // http.Get("http://foo.bar/pipo") // C is called, and so on 165 // 166 // A panic occurs if next is the result of another Then call (because 167 // allowing it could cause inextricable problems at runtime). Then 168 // calls can be chained, but cannot call each other by 169 // parameter. Example: 170 // A.Then(B).Then(C) // is OK 171 // A.Then(B.Then(C)) // panics as A.Then() parameter is another Then() call 172 func (r Responder) Then(next Responder) (x Responder) { 173 var done int 174 var mu sync.Mutex 175 x = func(req *http.Request) (*http.Response, error) { 176 mu.Lock() 177 defer mu.Unlock() 178 179 ctx := req.Context() 180 thenCalledUs, _ := ctx.Value(fromThenKey).(bool) 181 if !thenCalledUs { 182 req = req.WithContext(context.WithValue(ctx, fromThenKey, true)) 183 } 184 185 switch done { 186 case 0: 187 resp, err := r(req) 188 if err != errThenDone { 189 if !x.similar(r) { // r is NOT a Then 190 done = 1 191 } 192 return resp, err 193 } 194 fallthrough 195 196 case 1: 197 done = 2 // next is NEVER a Then, as it is forbidden by design 198 return next(req) 199 } 200 if thenCalledUs { 201 return nil, errThenDone 202 } 203 return next(req) 204 } 205 206 if next.similar(x) { 207 panic("Then() does not accept another Then() Responder as parameter") 208 } 209 return 210 } 211 212 // ResponderFromResponse wraps an *http.Response in a Responder. 213 // 214 // Be careful, except for responses generated by httpmock 215 // (NewStringResponse and NewBytesResponse functions) for which there 216 // is no problems, it is the caller responsibility to ensure the 217 // response body can be read several times and concurrently if needed, 218 // as it is shared among all Responder returned responses. 219 // 220 // For home-made responses, NewRespBodyFromString and 221 // NewRespBodyFromBytes functions can be used to produce response 222 // bodies that can be read several times and concurrently. 223 func ResponderFromResponse(resp *http.Response) Responder { 224 return func(req *http.Request) (*http.Response, error) { 225 res := *resp 226 227 // Our stuff: generate a new io.ReadCloser instance sharing the same buffer 228 if body, ok := resp.Body.(*dummyReadCloser); ok { 229 res.Body = body.copy() 230 } 231 232 res.Request = req 233 return &res, nil 234 } 235 } 236 237 // ResponderFromMultipleResponses wraps an *http.Response list in a Responder. 238 // 239 // Each response will be returned in the order of the provided list. 240 // If the responder is called more than the size of the provided list, an error 241 // will be thrown. 242 // 243 // Be careful, except for responses generated by httpmock 244 // (NewStringResponse and NewBytesResponse functions) for which there 245 // is no problems, it is the caller responsibility to ensure the 246 // response body can be read several times and concurrently if needed, 247 // as it is shared among all Responder returned responses. 248 // 249 // For home-made responses, NewRespBodyFromString and 250 // NewRespBodyFromBytes functions can be used to produce response 251 // bodies that can be read several times and concurrently. 252 // 253 // If all responses have been returned and fn is passed 254 // and non-nil, it acts as the fn parameter of NewNotFoundResponder, 255 // allowing to dump the stack trace to localize the origin of the 256 // call. 257 // import ( 258 // "github.com/jarcoal/httpmock" 259 // "testing" 260 // ) 261 // ... 262 // func TestMyApp(t *testing.T) { 263 // ... 264 // // This responder is callable only once, then an error is returned and 265 // // the stacktrace of the call logged using t.Log() 266 // httpmock.RegisterResponder("GET", "/foo/bar", 267 // httpmock.ResponderFromMultipleResponses( 268 // []*http.Response{ 269 // httpmock.NewStringResponse(200, `{"name":"bar"}`), 270 // httpmock.NewStringResponse(404, `{"mesg":"Not found"}`), 271 // }, 272 // t.Log), 273 // ) 274 // } 275 func ResponderFromMultipleResponses(responses []*http.Response, fn ...func(...any)) Responder { 276 responseIndex := 0 277 mutex := sync.Mutex{} 278 return func(req *http.Request) (*http.Response, error) { 279 mutex.Lock() 280 defer mutex.Unlock() 281 defer func() { responseIndex++ }() 282 if responseIndex >= len(responses) { 283 err := internal.StackTracer{ 284 Err: fmt.Errorf("not enough responses provided: responder called %d time(s) but %d response(s) provided", responseIndex+1, len(responses)), 285 } 286 if len(fn) > 0 { 287 err.CustomFn = fn[0] 288 } 289 return nil, err 290 } 291 res := *responses[responseIndex] 292 // Our stuff: generate a new io.ReadCloser instance sharing the same buffer 293 if body, ok := responses[responseIndex].Body.(*dummyReadCloser); ok { 294 res.Body = body.copy() 295 } 296 297 res.Request = req 298 return &res, nil 299 } 300 } 301 302 // NewErrorResponder creates a Responder that returns an empty request and the 303 // given error. This can be used to e.g. imitate more deep http errors for the 304 // client. 305 func NewErrorResponder(err error) Responder { 306 return func(req *http.Request) (*http.Response, error) { 307 return nil, err 308 } 309 } 310 311 // NewNotFoundResponder creates a Responder typically used in 312 // conjunction with RegisterNoResponder() function and testing 313 // package, to be proactive when a Responder is not found. fn is 314 // called with a unique string parameter containing the name of the 315 // missing route and the stack trace to localize the origin of the 316 // call. If fn returns (= if it does not panic), the responder returns 317 // an error of the form: "Responder not found for GET http://foo.bar/path". 318 // Note that fn can be nil. 319 // 320 // It is useful when writing tests to ensure that all routes have been 321 // mocked. 322 // 323 // Example of use: 324 // import ( 325 // "testing" 326 // "github.com/jarcoal/httpmock" 327 // ) 328 // ... 329 // func TestMyApp(t *testing.T) { 330 // ... 331 // // Calls testing.Fatal with the name of Responder-less route and 332 // // the stack trace of the call. 333 // httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal)) 334 // 335 // Will abort the current test and print something like: 336 // transport_test.go:735: Called from net/http.Get() 337 // at /go/src/github.com/jarcoal/httpmock/transport_test.go:714 338 // github.com/jarcoal/httpmock.TestCheckStackTracer() 339 // at /go/src/testing/testing.go:865 340 // testing.tRunner() 341 // at /go/src/runtime/asm_amd64.s:1337 342 func NewNotFoundResponder(fn func(...any)) Responder { 343 return func(req *http.Request) (*http.Response, error) { 344 var extra string 345 suggested, _ := req.Context().Value(suggestedKey).(*suggestedInfo) 346 if suggested != nil { 347 extra = fmt.Sprintf(`, but one matches %s %q`, suggested.kind, suggested.suggested) 348 } 349 return nil, internal.StackTracer{ 350 CustomFn: fn, 351 Err: fmt.Errorf("Responder not found for %s %s%s", req.Method, req.URL, extra), 352 } 353 } 354 } 355 356 // NewStringResponse creates an *http.Response with a body based on 357 // the given string. Also accepts an http status code. 358 // 359 // To pass the content of an existing file as body use httpmock.File as in: 360 // httpmock.NewStringResponse(200, httpmock.File("body.txt").String()) 361 func NewStringResponse(status int, body string) *http.Response { 362 return &http.Response{ 363 Status: strconv.Itoa(status), 364 StatusCode: status, 365 Body: NewRespBodyFromString(body), 366 Header: http.Header{}, 367 ContentLength: -1, 368 } 369 } 370 371 // NewStringResponder creates a Responder from a given body (as a string) and status code. 372 // 373 // To pass the content of an existing file as body use httpmock.File as in: 374 // httpmock.NewStringResponder(200, httpmock.File("body.txt").String()) 375 func NewStringResponder(status int, body string) Responder { 376 return ResponderFromResponse(NewStringResponse(status, body)) 377 } 378 379 // NewBytesResponse creates an *http.Response with a body based on the 380 // given bytes. Also accepts an http status code. 381 // 382 // To pass the content of an existing file as body use httpmock.File as in: 383 // httpmock.NewBytesResponse(200, httpmock.File("body.raw").Bytes()) 384 func NewBytesResponse(status int, body []byte) *http.Response { 385 return &http.Response{ 386 Status: strconv.Itoa(status), 387 StatusCode: status, 388 Body: NewRespBodyFromBytes(body), 389 Header: http.Header{}, 390 ContentLength: -1, 391 } 392 } 393 394 // NewBytesResponder creates a Responder from a given body (as a byte 395 // slice) and status code. 396 // 397 // To pass the content of an existing file as body use httpmock.File as in: 398 // httpmock.NewBytesResponder(200, httpmock.File("body.raw").Bytes()) 399 func NewBytesResponder(status int, body []byte) Responder { 400 return ResponderFromResponse(NewBytesResponse(status, body)) 401 } 402 403 // NewJsonResponse creates an *http.Response with a body that is a 404 // json encoded representation of the given any. Also accepts 405 // an http status code. 406 // 407 // To pass the content of an existing file as body use httpmock.File as in: 408 // httpmock.NewJsonResponse(200, httpmock.File("body.json")) 409 func NewJsonResponse(status int, body any) (*http.Response, error) { // nolint: revive 410 encoded, err := json.Marshal(body) 411 if err != nil { 412 return nil, err 413 } 414 response := NewBytesResponse(status, encoded) 415 response.Header.Set("Content-Type", "application/json") 416 return response, nil 417 } 418 419 // NewJsonResponder creates a Responder from a given body (as an 420 // any that is encoded to json) and status code. 421 // 422 // To pass the content of an existing file as body use httpmock.File as in: 423 // httpmock.NewJsonResponder(200, httpmock.File("body.json")) 424 func NewJsonResponder(status int, body any) (Responder, error) { // nolint: revive 425 resp, err := NewJsonResponse(status, body) 426 if err != nil { 427 return nil, err 428 } 429 return ResponderFromResponse(resp), nil 430 } 431 432 // NewJsonResponderOrPanic is like NewJsonResponder but panics in case of error. 433 // 434 // It simplifies the call of RegisterResponder, avoiding the use of a 435 // temporary variable and an error check, and so can be used as 436 // NewStringResponder or NewBytesResponder in such context: 437 // httpmock.RegisterResponder( 438 // "GET", 439 // "/test/path", 440 // httpmock.NewJSONResponderOrPanic(200, &MyBody), 441 // ) 442 // 443 // To pass the content of an existing file as body use httpmock.File as in: 444 // httpmock.NewJsonResponderOrPanic(200, httpmock.File("body.json")) 445 func NewJsonResponderOrPanic(status int, body any) Responder { // nolint: revive 446 responder, err := NewJsonResponder(status, body) 447 if err != nil { 448 panic(err) 449 } 450 return responder 451 } 452 453 // NewXmlResponse creates an *http.Response with a body that is an xml 454 // encoded representation of the given any. Also accepts an 455 // http status code. 456 // 457 // To pass the content of an existing file as body use httpmock.File as in: 458 // httpmock.NewXmlResponse(200, httpmock.File("body.xml")) 459 func NewXmlResponse(status int, body any) (*http.Response, error) { // nolint: revive 460 var ( 461 encoded []byte 462 err error 463 ) 464 if f, ok := body.(File); ok { 465 encoded, err = f.bytes() 466 } else { 467 encoded, err = xml.Marshal(body) 468 } 469 if err != nil { 470 return nil, err 471 } 472 response := NewBytesResponse(status, encoded) 473 response.Header.Set("Content-Type", "application/xml") 474 return response, nil 475 } 476 477 // NewXmlResponder creates a Responder from a given body (as an 478 // any that is encoded to xml) and status code. 479 // 480 // To pass the content of an existing file as body use httpmock.File as in: 481 // httpmock.NewXmlResponder(200, httpmock.File("body.xml")) 482 func NewXmlResponder(status int, body any) (Responder, error) { // nolint: revive 483 resp, err := NewXmlResponse(status, body) 484 if err != nil { 485 return nil, err 486 } 487 return ResponderFromResponse(resp), nil 488 } 489 490 // NewXmlResponderOrPanic is like NewXmlResponder but panics in case of error. 491 // 492 // It simplifies the call of RegisterResponder, avoiding the use of a 493 // temporary variable and an error check, and so can be used as 494 // NewStringResponder or NewBytesResponder in such context: 495 // httpmock.RegisterResponder( 496 // "GET", 497 // "/test/path", 498 // httpmock.NewXmlResponderOrPanic(200, &MyBody), 499 // ) 500 // 501 // To pass the content of an existing file as body use httpmock.File as in: 502 // httpmock.NewXmlResponderOrPanic(200, httpmock.File("body.xml")) 503 func NewXmlResponderOrPanic(status int, body any) Responder { // nolint: revive 504 responder, err := NewXmlResponder(status, body) 505 if err != nil { 506 panic(err) 507 } 508 return responder 509 } 510 511 // NewRespBodyFromString creates an io.ReadCloser from a string that 512 // is suitable for use as an http response body. 513 // 514 // To pass the content of an existing file as body use httpmock.File as in: 515 // httpmock.NewRespBodyFromString(httpmock.File("body.txt").String()) 516 func NewRespBodyFromString(body string) io.ReadCloser { 517 return &dummyReadCloser{orig: body} 518 } 519 520 // NewRespBodyFromBytes creates an io.ReadCloser from a byte slice 521 // that is suitable for use as an http response body. 522 // 523 // To pass the content of an existing file as body use httpmock.File as in: 524 // httpmock.NewRespBodyFromBytes(httpmock.File("body.txt").Bytes()) 525 func NewRespBodyFromBytes(body []byte) io.ReadCloser { 526 return &dummyReadCloser{orig: body} 527 } 528 529 type dummyReadCloser struct { 530 orig any // string or []byte 531 body io.ReadSeeker // instanciated on demand from orig 532 } 533 534 // copy returns a new instance resetting d.body to nil. 535 func (d *dummyReadCloser) copy() *dummyReadCloser { 536 return &dummyReadCloser{orig: d.orig} 537 } 538 539 // setup ensures d.body is correctly initialized. 540 func (d *dummyReadCloser) setup() { 541 if d.body == nil { 542 switch body := d.orig.(type) { 543 case string: 544 d.body = strings.NewReader(body) 545 case []byte: 546 d.body = bytes.NewReader(body) 547 } 548 } 549 } 550 551 func (d *dummyReadCloser) Read(p []byte) (n int, err error) { 552 d.setup() 553 return d.body.Read(p) 554 } 555 556 func (d *dummyReadCloser) Close() error { 557 d.setup() 558 d.body.Seek(0, io.SeekEnd) // nolint: errcheck 559 return nil 560 } 561