github.com/avenga/couper@v1.12.2/server/http_backend_test.go (about) 1 package server_test 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "context" 7 "encoding/json" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "strconv" 12 "sync" 13 "sync/atomic" 14 "testing" 15 "time" 16 17 "github.com/sirupsen/logrus" 18 19 "github.com/avenga/couper/internal/test" 20 "github.com/avenga/couper/logging" 21 ) 22 23 func TestBackend_MaxConnections(t *testing.T) { 24 helper := test.New(t) 25 26 const reqCount = 3 27 lastSeen := map[string]string{} 28 lastSeenMu := sync.Mutex{} 29 origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 30 lastSeenMu.Lock() 31 defer lastSeenMu.Unlock() 32 33 if lastSeen[r.URL.Path] != "" && lastSeen[r.URL.Path] != r.RemoteAddr { 34 t.Errorf("expected same remote addr for path: %q", r.URL.Path) 35 rw.WriteHeader(http.StatusInternalServerError) 36 } else { 37 rw.WriteHeader(http.StatusNoContent) 38 } 39 lastSeen[r.URL.Path] = r.RemoteAddr 40 })) 41 42 defer origin.Close() 43 44 shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/03_couper.hcl", helper, map[string]interface{}{ 45 "origin": origin.URL, 46 }) 47 helper.Must(cerr) 48 defer shutdown() 49 50 paths := []string{ 51 "/", 52 "/be", 53 "/fake-sequence", 54 } 55 56 originWait := sync.WaitGroup{} 57 originWait.Add(len(paths) * reqCount) 58 waitForCh := make(chan struct{}) 59 60 client := test.NewHTTPClient() 61 62 for _, clientPath := range paths { 63 for i := 0; i < reqCount; i++ { 64 go func(path string) { 65 req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+path, nil) 66 <-waitForCh 67 res, err := client.Do(req) 68 helper.Must(err) 69 70 if res.StatusCode != http.StatusNoContent { 71 t.Errorf("want: 204, got %d", res.StatusCode) 72 } 73 74 originWait.Done() 75 }(clientPath) 76 } 77 } 78 79 close(waitForCh) 80 originWait.Wait() 81 } 82 83 func TestBackend_MaxConnections_BodyClose(t *testing.T) { 84 helper := test.New(t) 85 86 origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 87 time.Sleep(time.Second) // always delay, ensures every req hit runs into max_conns issue 88 89 rw.Header().Set("Content-Type", "application/json") 90 b, err := json.Marshal(r.URL) 91 helper.Must(err) 92 _, err = rw.Write(b) 93 helper.Must(err) 94 })) 95 96 defer origin.Close() 97 98 shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/04_couper.hcl", helper, 99 map[string]interface{}{ 100 "origin": origin.URL, 101 }) 102 helper.Must(cerr) 103 defer shutdown() 104 105 client := test.NewHTTPClient() 106 107 paths := []string{ 108 "/", 109 "/named", 110 "/default", 111 "/default2", 112 "/ws", 113 "/proxy-seq", 114 "/proxy-seq-ref", 115 } 116 117 t.Run("parallel", func(t *testing.T) { 118 for _, path := range paths { 119 p := path // we need a local copy due to ref in parallel test func 120 t.Run("_"+p, func(st *testing.T) { 121 st.Parallel() 122 123 h := test.New(st) 124 125 req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+p, nil) 126 127 deadline, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(len(paths)*10)) 128 defer cancel() 129 res, err := client.Do(req.WithContext(deadline)) 130 h.Must(err) 131 132 if res.StatusCode != http.StatusOK { 133 st.Errorf("want: 200, got %d", res.StatusCode) 134 } 135 136 _, err = io.Copy(io.Discard, res.Body) 137 h.Must(err) 138 h.Must(res.Body.Close()) 139 }) 140 } 141 }) 142 } 143 144 // TestBackend_WithoutOrigin expects the listed errors to ensure no host from the client-request 145 // leaks into the backend structure for connecting to the origin. 146 func TestBackend_WithoutOrigin(t *testing.T) { 147 helper := test.New(t) 148 shutdown, hook := newCouper("testdata/integration/backends/01_couper.hcl", helper) 149 defer shutdown() 150 151 client := test.NewHTTPClient() 152 153 for _, tc := range []struct { 154 path string 155 message string 156 }{ 157 {"/proxy/backend-path", `configuration error: anonymous_6_13: the origin attribute has to contain an absolute URL with a valid hostname: ""`}, 158 {"/proxy/url", `configuration error: anonymous_15_13: the origin attribute has to contain an absolute URL with a valid hostname: ""`}, 159 {"/request/backend-path", `configuration error: anonymous_28_15: the origin attribute has to contain an absolute URL with a valid hostname: ""`}, 160 {"/request/url", `configuration error: anonymous_37_15: the origin attribute has to contain an absolute URL with a valid hostname: ""`}, 161 } { 162 t.Run(tc.path, func(st *testing.T) { 163 hook.Reset() 164 165 h := test.New(st) 166 req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+tc.path, nil) 167 res, err := client.Do(req) 168 h.Must(err) 169 170 if res.StatusCode != http.StatusInternalServerError { 171 st.Errorf("want: 500, got %d", res.StatusCode) 172 } 173 174 for _, e := range hook.AllEntries() { 175 if e.Level != logrus.ErrorLevel { 176 continue 177 } 178 179 if e.Message != tc.message { 180 st.Errorf("\nwant: %q\ngot: %q\n", tc.message, e.Message) 181 } 182 } 183 184 }) 185 186 } 187 } 188 189 func TestBackend_LogResponseBytes(t *testing.T) { 190 helper := test.New(t) 191 192 var writtenBytes int64 193 origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 194 rw.Header().Set("Content-Type", "application/json") 195 b, err := json.Marshal(r.URL) 196 helper.Must(err) 197 198 if r.Header.Get("Accept-Encoding") == "gzip" { 199 buf := &bytes.Buffer{} 200 gw := gzip.NewWriter(buf) 201 _, zerr := gw.Write(b) 202 helper.Must(zerr) 203 helper.Must(gw.Close()) 204 205 atomic.StoreInt64(&writtenBytes, int64(buf.Len())) 206 207 _, err = io.Copy(rw, buf) 208 helper.Must(err) 209 } else { 210 n, werr := rw.Write(b) 211 helper.Must(werr) 212 atomic.StoreInt64(&writtenBytes, int64(n)) 213 } 214 })) 215 216 defer origin.Close() 217 218 shutdown, hook, cerr := newCouperWithTemplate("testdata/integration/backends/05_couper.hcl", helper, 219 map[string]interface{}{ 220 "origin": origin.URL, 221 }) 222 helper.Must(cerr) 223 defer shutdown() 224 225 client := test.NewHTTPClient() 226 227 cases := []struct { 228 accept string 229 path string 230 }{ 231 {path: "/"}, 232 {accept: "gzip", path: "/zipped"}, 233 } 234 235 for _, tc := range cases { 236 hook.Reset() 237 238 deadline, cancel := context.WithTimeout(context.Background(), time.Second*10) 239 240 req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+tc.path, nil) 241 242 if tc.accept != "" { 243 req.Header.Set("Accept-Encoding", tc.accept) 244 } 245 246 res, err := client.Do(req.WithContext(deadline)) 247 cancel() 248 helper.Must(err) 249 250 if res.StatusCode != http.StatusOK { 251 t.Errorf("want: 200, got %d", res.StatusCode) 252 } 253 254 _, err = io.Copy(io.Discard, res.Body) 255 helper.Must(err) 256 257 helper.Must(res.Body.Close()) 258 259 var seen bool 260 for _, e := range hook.AllEntries() { 261 if e.Data["type"] != "couper_backend" { 262 continue 263 } 264 265 seen = true 266 267 response, ok := e.Data["response"] 268 if !ok { 269 t.Error("expected response log field") 270 } 271 272 bytesValue, bok := response.(logging.Fields)["bytes"] 273 if !bok { 274 t.Error("expected response.bytes log field") 275 } 276 277 expectedBytes := atomic.LoadInt64(&writtenBytes) 278 if bytesValue.(int64) != expectedBytes { 279 t.Errorf("bytes differs: want: %d, got: %d", expectedBytes, bytesValue) 280 } 281 } 282 283 if !seen { 284 t.Error("expected upstream log") 285 } 286 } 287 } 288 289 func TestBackend_Unhealthy(t *testing.T) { 290 helper := test.New(t) 291 292 var unhealthy int64 293 origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 294 if counter := r.Header.Get("Counter"); counter != "" { 295 c, _ := strconv.Atoi(counter) 296 if c > 2 { 297 atomic.StoreInt64(&unhealthy, 1) 298 } 299 } 300 if atomic.LoadInt64(&unhealthy) == 1 { 301 rw.WriteHeader(http.StatusConflict) 302 } else { 303 rw.WriteHeader(http.StatusNoContent) 304 time.Sleep(time.Second / 3) 305 } 306 })) 307 308 defer origin.Close() 309 310 shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/06_couper.hcl", helper, 311 map[string]interface{}{ 312 "origin": origin.URL, 313 }) 314 helper.Must(cerr) 315 defer shutdown() 316 317 client := test.NewHTTPClient() 318 319 type testcase struct { 320 path string 321 expStatus int 322 } 323 324 for i, tc := range []testcase{ 325 {"/anon", http.StatusNoContent}, 326 {"/ref", http.StatusNoContent}, 327 {"/catch", http.StatusNoContent}, 328 // server switched resp status-code -> unhealthy 329 {"/anon", http.StatusConflict}, // always healthy 330 {"/ref", http.StatusBadGateway}, 331 {"/catch", http.StatusTeapot}, 332 } { 333 t.Run(tc.path, func(st *testing.T) { 334 h := test.New(st) 335 req, err := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+tc.path, nil) 336 h.Must(err) 337 req.Header.Set("Counter", strconv.Itoa(i)) 338 res, err := client.Do(req) 339 h.Must(err) 340 341 if res.StatusCode != tc.expStatus { 342 st.Errorf("want status %d, got: %d", tc.expStatus, res.StatusCode) 343 } 344 }) 345 } 346 } 347 348 func TestBackend_Oauth2_TokenEndpoint(t *testing.T) { 349 helper := test.New(t) 350 351 requestCount := 0 352 origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 353 rw.Header().Set("Content-Type", "application/json") 354 rw.WriteHeader(http.StatusUnauthorized) 355 _, werr := rw.Write([]byte(`{"path": "` + r.URL.Path + `"}`)) 356 requestCount++ 357 helper.Must(werr) 358 })) 359 defer origin.Close() 360 361 tokenEndpoint := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 362 rw.Header().Set("Content-Type", "application/json") 363 _, werr := rw.Write([]byte(`{ 364 "access_token": "my-token", 365 "expires_in": 120 366 }`)) 367 helper.Must(werr) 368 })) 369 defer tokenEndpoint.Close() 370 371 retries := 3 372 shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/07_couper.hcl", helper, 373 map[string]interface{}{ 374 "origin": origin.URL, 375 "token_endpoint": tokenEndpoint.URL, 376 "retries": retries, 377 }) 378 helper.Must(cerr) 379 defer shutdown() 380 381 client := test.NewHTTPClient() 382 383 req, err := http.NewRequest(http.MethodGet, "http://couper.dev:8080/test-path", nil) 384 helper.Must(err) 385 res, err := client.Do(req) 386 helper.Must(err) 387 388 if res.StatusCode != http.StatusUnauthorized { 389 t.Errorf("want status %d, got: %d", http.StatusUnauthorized, res.StatusCode) 390 } 391 392 if res.Header.Get("Content-Type") != "application/json" { 393 t.Errorf("want json content-type") 394 return 395 } 396 397 type result struct { 398 Path string 399 } 400 401 b, err := io.ReadAll(res.Body) 402 helper.Must(err) 403 helper.Must(res.Body.Close()) 404 405 r := &result{} 406 helper.Must(json.Unmarshal(b, r)) 407 408 if r.Path != "/test-path" { 409 t.Errorf("path property want: %q, got: %q", "/test-path", r.Path) 410 } 411 412 if requestCount != retries+1 { 413 t.Errorf("unexpected number of requests, want: %d, got: %d", retries+1, requestCount) 414 } 415 } 416 417 func TestBackend_BackendVar(t *testing.T) { 418 helper := test.New(t) 419 shutdown, hook := newCouper("testdata/integration/backends/08_couper.hcl", helper) 420 defer shutdown() 421 422 client := test.NewHTTPClient() 423 424 hook.Reset() 425 426 req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080/anything", nil) 427 res, err := client.Do(req) 428 helper.Must(err) 429 430 hHealthy1 := res.Header.Get("x-healthy-1") 431 hHealthy2 := res.Header.Get("x-healthy-2") 432 if hHealthy1 != "true" { 433 t.Errorf("expected x-healthy-1 to be true, got %q", hHealthy1) 434 } 435 if hHealthy2 != "true" { 436 t.Errorf("expected x-healthy-2 to be true, got %q", hHealthy2) 437 } 438 hRequestPath1 := res.Header.Get("x-rp-1") 439 hRequestPath2 := res.Header.Get("x-rp-2") 440 if hRequestPath1 != "/anything" { 441 t.Errorf("expected x-rp-1 to be %q, got %q", "/anything", hRequestPath1) 442 } 443 if hRequestPath2 != "/anything" { 444 t.Errorf("expected x-rp-2 to be %q, got %q", "/anything", hRequestPath2) 445 } 446 hResponseStatus1 := res.Header.Get("x-rs-1") 447 hResponseStatus2 := res.Header.Get("x-rs-2") 448 if hResponseStatus1 != "200" { 449 t.Errorf("expected x-rs-1 to be %q, got %q", "/200", hResponseStatus1) 450 } 451 if hResponseStatus2 != "200" { 452 t.Errorf("expected x-rs-2 to be %q, got %q", "/200", hResponseStatus2) 453 } 454 455 for _, e := range hook.AllEntries() { 456 if e.Data["type"] != "couper_backend" { 457 continue 458 } 459 custom, _ := e.Data["custom"].(logrus.Fields) 460 461 if lHealthy1, ok := custom["healthy_1"].(bool); !ok { 462 t.Error("expected healthy_1 to be set and bool") 463 } else if lHealthy1 != true { 464 t.Errorf("expected healthy_1 to be true, got %v", lHealthy1) 465 } 466 if lHealthy2, ok := custom["healthy_2"].(bool); !ok { 467 t.Error("expected healthy_2 to be set and bool") 468 } else if lHealthy2 != true { 469 t.Errorf("expected healthy_2 to be true, got %v", lHealthy2) 470 } 471 472 if lRequestPath1, ok := custom["rp_1"].(string); !ok { 473 t.Error("expected rp_1 to be set and string") 474 } else if lRequestPath1 != "/anything" { 475 t.Errorf("expected rp_1 to be %q, got %v", "/anything", lRequestPath1) 476 } 477 if lRequestPath2, ok := custom["rp_2"].(string); !ok { 478 t.Error("expected rp_2 to be set and string") 479 } else if lRequestPath2 != "/anything" { 480 t.Errorf("expected rp_2 to be %q, got %v", "/anything", lRequestPath2) 481 } 482 483 if lResponseStatus1, ok := custom["rs_1"].(float64); !ok { 484 t.Error("expected rs_1 to be set and float64") 485 } else if lResponseStatus1 != 200 { 486 t.Errorf("expected rs_1 to be %d, got %v", 200, lResponseStatus1) 487 } 488 if lResponseStatus2, ok := custom["rs_2"].(float64); !ok { 489 t.Error("expected rs_2 to be set and float64") 490 } else if lResponseStatus2 != 200 { 491 t.Errorf("expected rs_2 to be %d, got %v", 200, lResponseStatus2) 492 } 493 } 494 }