github.com/Jeffail/benthos/v3@v3.65.0/internal/http/client_test.go (about) 1 package http 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "mime" 9 "mime/multipart" 10 "net/http" 11 "net/http/httptest" 12 "net/textproto" 13 "strconv" 14 "strings" 15 "sync/atomic" 16 "testing" 17 "time" 18 19 "github.com/Jeffail/benthos/v3/lib/log" 20 "github.com/Jeffail/benthos/v3/lib/message" 21 "github.com/Jeffail/benthos/v3/lib/metrics" 22 "github.com/Jeffail/benthos/v3/lib/types" 23 "github.com/Jeffail/benthos/v3/lib/util/http/client" 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/require" 26 ) 27 28 //------------------------------------------------------------------------------ 29 30 func TestHTTPClientRetries(t *testing.T) { 31 var reqCount uint32 32 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 atomic.AddUint32(&reqCount, 1) 34 http.Error(w, "test error", http.StatusForbidden) 35 })) 36 defer ts.Close() 37 38 conf := client.NewConfig() 39 conf.URL = ts.URL + "/testpost" 40 conf.Retry = "1ms" 41 conf.NumRetries = 3 42 43 h, err := NewClient(conf) 44 require.NoError(t, err) 45 defer h.Close(context.Background()) 46 47 out := message.New([][]byte{[]byte("test")}) 48 _, err = h.Send(context.Background(), out, out) 49 assert.Error(t, err) 50 assert.Equal(t, uint32(4), atomic.LoadUint32(&reqCount)) 51 } 52 53 func TestHTTPClientBadRequest(t *testing.T) { 54 conf := client.NewConfig() 55 conf.URL = "htp://notvalid:1111" 56 conf.Verb = "notvalid\n" 57 conf.NumRetries = 3 58 59 h, err := NewClient(conf) 60 require.NoError(t, err) 61 62 out := message.New([][]byte{[]byte("test")}) 63 _, err = h.Send(context.Background(), out, out) 64 assert.Error(t, err) 65 } 66 67 func TestHTTPClientSendBasic(t *testing.T) { 68 nTestLoops := 1000 69 70 resultChan := make(chan types.Message, 1) 71 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 msg := message.New(nil) 73 defer func() { 74 resultChan <- msg 75 }() 76 77 b, err := io.ReadAll(r.Body) 78 require.NoError(t, err) 79 80 msg.Append(message.NewPart(b)) 81 })) 82 defer ts.Close() 83 84 conf := client.NewConfig() 85 conf.URL = ts.URL + "/testpost" 86 87 h, err := NewClient(conf) 88 require.NoError(t, err) 89 90 for i := 0; i < nTestLoops; i++ { 91 testStr := fmt.Sprintf("test%v", i) 92 testMsg := message.New([][]byte{[]byte(testStr)}) 93 94 _, err = h.Send(context.Background(), testMsg, testMsg) 95 require.NoError(t, err) 96 97 select { 98 case resMsg := <-resultChan: 99 require.Equal(t, 1, resMsg.Len()) 100 assert.Equal(t, testStr, string(resMsg.Get(0).Get())) 101 case <-time.After(time.Second): 102 t.Fatal("Action timed out") 103 } 104 } 105 } 106 107 func TestHTTPClientBadContentType(t *testing.T) { 108 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 109 b, err := io.ReadAll(r.Body) 110 require.NoError(t, err) 111 112 _, err = w.Write(bytes.ToUpper(b)) 113 require.NoError(t, err) 114 })) 115 t.Cleanup(ts.Close) 116 117 conf := client.NewConfig() 118 conf.URL = ts.URL + "/testpost" 119 120 h, err := NewClient(conf) 121 require.NoError(t, err) 122 123 testMsg := message.New([][]byte{[]byte("hello world")}) 124 125 res, err := h.Send(context.Background(), testMsg, testMsg) 126 require.NoError(t, err) 127 128 require.Equal(t, 1, res.Len()) 129 assert.Equal(t, "HELLO WORLD", string(res.Get(0).Get())) 130 } 131 132 func TestHTTPClientDropOn(t *testing.T) { 133 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 134 w.WriteHeader(http.StatusBadRequest) 135 w.Write([]byte(`{"foo":"bar"}`)) 136 })) 137 defer ts.Close() 138 139 conf := client.NewConfig() 140 conf.URL = ts.URL + "/testpost" 141 conf.DropOn = []int{400} 142 143 h, err := NewClient(conf) 144 require.NoError(t, err) 145 146 testMsg := message.New([][]byte{[]byte(`{"bar":"baz"}`)}) 147 148 _, err = h.Send(context.Background(), testMsg, testMsg) 149 require.Error(t, err) 150 } 151 152 func TestHTTPClientSuccessfulOn(t *testing.T) { 153 var reqs int32 154 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 155 w.WriteHeader(http.StatusBadRequest) 156 w.Write([]byte(`{"foo":"bar"}`)) 157 atomic.AddInt32(&reqs, 1) 158 })) 159 defer ts.Close() 160 161 conf := client.NewConfig() 162 conf.URL = ts.URL + "/testpost" 163 conf.SuccessfulOn = []int{400} 164 165 h, err := NewClient(conf) 166 require.NoError(t, err) 167 168 testMsg := message.New([][]byte{[]byte(`{"bar":"baz"}`)}) 169 resMsg, err := h.Send(context.Background(), testMsg, testMsg) 170 require.NoError(t, err) 171 172 assert.Equal(t, `{"foo":"bar"}`, string(resMsg.Get(0).Get())) 173 assert.Equal(t, int32(1), atomic.LoadInt32(&reqs)) 174 } 175 176 func TestHTTPClientSendInterpolate(t *testing.T) { 177 nTestLoops := 1000 178 179 resultChan := make(chan types.Message, 1) 180 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 181 assert.Equal(t, "/firstvar", r.URL.Path) 182 assert.Equal(t, "hdr-secondvar", r.Header.Get("dynamic")) 183 assert.Equal(t, "foo", r.Header.Get("static")) 184 assert.Equal(t, "simpleHost.com", r.Host) 185 186 msg := message.New(nil) 187 defer func() { 188 resultChan <- msg 189 }() 190 191 b, err := io.ReadAll(r.Body) 192 require.NoError(t, err) 193 194 msg.Append(message.NewPart(b)) 195 })) 196 defer ts.Close() 197 198 conf := client.NewConfig() 199 conf.URL = ts.URL + `/${! json("foo.bar") }` 200 conf.Headers["static"] = "foo" 201 conf.Headers["dynamic"] = `hdr-${!json("foo.baz")}` 202 conf.Headers["Host"] = "simpleHost.com" 203 204 h, err := NewClient(conf, OptSetLogger(log.Noop()), OptSetStats(metrics.Noop())) 205 require.NoError(t, err) 206 207 for i := 0; i < nTestLoops; i++ { 208 testStr := fmt.Sprintf(`{"test":%v,"foo":{"bar":"firstvar","baz":"secondvar"}}`, i) 209 testMsg := message.New([][]byte{[]byte(testStr)}) 210 211 _, err = h.Send(context.Background(), testMsg, testMsg) 212 require.NoError(t, err) 213 214 select { 215 case resMsg := <-resultChan: 216 require.Equal(t, 1, resMsg.Len()) 217 assert.Equal(t, testStr, string(resMsg.Get(0).Get())) 218 case <-time.After(time.Second): 219 t.Fatal("Action timed out") 220 } 221 } 222 } 223 224 func TestHTTPClientSendMultipart(t *testing.T) { 225 nTestLoops := 1000 226 227 resultChan := make(chan types.Message, 1) 228 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 229 msg := message.New(nil) 230 defer func() { 231 resultChan <- msg 232 }() 233 234 mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 235 require.NoError(t, err) 236 237 if strings.HasPrefix(mediaType, "multipart/") { 238 mr := multipart.NewReader(r.Body, params["boundary"]) 239 for { 240 p, err := mr.NextPart() 241 if err == io.EOF { 242 break 243 } 244 require.NoError(t, err) 245 246 msgBytes, err := io.ReadAll(p) 247 require.NoError(t, err) 248 249 msg.Append(message.NewPart(msgBytes)) 250 } 251 } else { 252 b, err := io.ReadAll(r.Body) 253 require.NoError(t, err) 254 255 msg.Append(message.NewPart(b)) 256 } 257 })) 258 defer ts.Close() 259 260 conf := client.NewConfig() 261 conf.URL = ts.URL + "/testpost" 262 263 h, err := NewClient(conf) 264 require.NoError(t, err) 265 266 for i := 0; i < nTestLoops; i++ { 267 testStr := fmt.Sprintf("test%v", i) 268 testMsg := message.New([][]byte{ 269 []byte(testStr + "PART-A"), 270 []byte(testStr + "PART-B"), 271 }) 272 273 _, err = h.Send(context.Background(), testMsg, testMsg) 274 require.NoError(t, err) 275 276 select { 277 case resMsg := <-resultChan: 278 assert.Equal(t, 2, resMsg.Len()) 279 assert.Equal(t, testStr+"PART-A", string(resMsg.Get(0).Get())) 280 assert.Equal(t, testStr+"PART-B", string(resMsg.Get(1).Get())) 281 case <-time.After(time.Second): 282 t.Fatal("Action timed out") 283 } 284 } 285 } 286 287 func TestHTTPClientReceive(t *testing.T) { 288 nTestLoops := 1000 289 290 j := 0 291 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 292 testStr := fmt.Sprintf("test%v", j) 293 j++ 294 w.Header().Set("foo-bar", "baz-0") 295 w.WriteHeader(http.StatusCreated) 296 w.Write([]byte(testStr + "PART-A")) 297 })) 298 defer ts.Close() 299 300 conf := client.NewConfig() 301 conf.URL = ts.URL + "/testpost" 302 303 h, err := NewClient(conf) 304 require.NoError(t, err) 305 306 for i := 0; i < nTestLoops; i++ { 307 testStr := fmt.Sprintf("test%v", j) 308 resMsg, err := h.Send(context.Background(), nil, nil) 309 require.NoError(t, err) 310 311 assert.Equal(t, 1, resMsg.Len()) 312 assert.Equal(t, testStr+"PART-A", string(resMsg.Get(0).Get())) 313 assert.Equal(t, "", resMsg.Get(0).Metadata().Get("foo-bar")) 314 assert.Equal(t, "201", resMsg.Get(0).Metadata().Get("http_status_code")) 315 } 316 } 317 318 func TestHTTPClientSendMetaFilter(t *testing.T) { 319 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 320 w.WriteHeader(http.StatusOK) 321 fmt.Fprintf(w, ` 322 foo_a: %v 323 bar_a: %v 324 foo_b: %v 325 bar_b: %v 326 `, 327 r.Header.Get("foo_a"), 328 r.Header.Get("bar_a"), 329 r.Header.Get("foo_b"), 330 r.Header.Get("bar_b"), 331 ) 332 })) 333 defer ts.Close() 334 335 conf := client.NewConfig() 336 conf.URL = ts.URL + "/testpost" 337 conf.Metadata.IncludePrefixes = []string{"foo_"} 338 339 h, err := NewClient(conf) 340 require.NoError(t, err) 341 342 sendMsg := message.New([][]byte{[]byte("hello world")}) 343 meta := sendMsg.Get(0).Metadata() 344 meta.Set("foo_a", "foo a value") 345 meta.Set("foo_b", "foo b value") 346 meta.Set("bar_a", "bar a value") 347 meta.Set("bar_b", "bar b value") 348 349 resMsg, err := h.Send(context.Background(), sendMsg, sendMsg) 350 require.NoError(t, err) 351 352 assert.Equal(t, 1, resMsg.Len()) 353 assert.Equal(t, ` 354 foo_a: foo a value 355 bar_a: 356 foo_b: foo b value 357 bar_b: 358 `, string(resMsg.Get(0).Get())) 359 } 360 361 func TestHTTPClientReceiveHeaders(t *testing.T) { 362 nTestLoops := 1000 363 364 j := 0 365 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 366 testStr := fmt.Sprintf("test%v", j) 367 j++ 368 w.Header().Set("foo-bar", "baz-0") 369 w.WriteHeader(http.StatusCreated) 370 w.Write([]byte(testStr + "PART-A")) 371 })) 372 defer ts.Close() 373 374 conf := client.NewConfig() 375 conf.URL = ts.URL + "/testpost" 376 conf.CopyResponseHeaders = true 377 378 h, err := NewClient(conf) 379 require.NoError(t, err) 380 381 for i := 0; i < nTestLoops; i++ { 382 testStr := fmt.Sprintf("test%v", j) 383 resMsg, err := h.Send(context.Background(), nil, nil) 384 require.NoError(t, err) 385 386 assert.Equal(t, 1, resMsg.Len()) 387 assert.Equal(t, testStr+"PART-A", string(resMsg.Get(0).Get())) 388 assert.Equal(t, "baz-0", resMsg.Get(0).Metadata().Get("foo-bar")) 389 assert.Equal(t, "201", resMsg.Get(0).Metadata().Get("http_status_code")) 390 } 391 } 392 393 func TestHTTPClientReceiveHeadersWithMetadataFiltering(t *testing.T) { 394 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 395 w.Header().Set("foobar", "baz") 396 w.Header().Set("extra", "val") 397 w.WriteHeader(http.StatusCreated) 398 })) 399 defer ts.Close() 400 401 conf := client.NewConfig() 402 conf.URL = ts.URL 403 404 for _, tt := range []struct { 405 name string 406 noExtraMetadata bool 407 copyResponseHeaders bool 408 includePrefixes []string 409 includePatterns []string 410 }{ 411 { 412 name: "no extra metadata", 413 noExtraMetadata: true, 414 }, 415 { 416 name: "copy_response_headers only", 417 copyResponseHeaders: true, 418 }, 419 { 420 name: "include_prefixes only", 421 includePrefixes: []string{"foo"}, 422 }, 423 { 424 name: "include_patterns only", 425 includePatterns: []string{".*bar"}, 426 }, 427 { 428 name: "both copy_response_headers and include_prefixes", 429 copyResponseHeaders: true, 430 includePrefixes: []string{"foo"}, 431 }, 432 } { 433 conf.CopyResponseHeaders = tt.copyResponseHeaders 434 conf.ExtractMetadata.IncludePrefixes = tt.includePrefixes 435 conf.ExtractMetadata.IncludePatterns = tt.includePatterns 436 h, err := NewClient(conf) 437 if err != nil { 438 t.Fatalf("%s: %s", tt.name, err) 439 } 440 441 resMsg, err := h.Send(context.Background(), nil, nil) 442 if err != nil { 443 t.Fatalf("%s: %s", tt.name, err) 444 } 445 446 metadataCount := 0 447 resMsg.Get(0).Metadata().Iter(func(_, _ string) error { metadataCount++; return nil }) 448 449 if tt.noExtraMetadata { 450 if metadataCount > 1 { 451 t.Errorf("%s: wrong number of metadata items: %d", tt.name, metadataCount) 452 } 453 if exp, act := "", resMsg.Get(0).Metadata().Get("foobar"); exp != act { 454 t.Errorf("%s: wrong metadata value: %v != %v", tt.name, act, exp) 455 } 456 } else if exp, act := "baz", resMsg.Get(0).Metadata().Get("foobar"); exp != act { 457 t.Errorf("%s: wrong metadata value: %v != %v", tt.name, act, exp) 458 } else if tt.copyResponseHeaders && h.metaExtractFilter.IsSet() { 459 if metadataCount < 3 { 460 t.Errorf("%s: wrong number of metadata items: %d", tt.name, metadataCount) 461 } 462 if exp, act := "val", resMsg.Get(0).Metadata().Get("extra"); exp != act { 463 t.Errorf("%s: wrong metadata value: %v != %v", tt.name, act, exp) 464 } 465 } 466 } 467 } 468 469 func TestHTTPClientReceiveMultipart(t *testing.T) { 470 nTestLoops := 1000 471 472 j := 0 473 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 474 testStr := fmt.Sprintf("test%v", j) 475 j++ 476 msg := message.New([][]byte{ 477 []byte(testStr + "PART-A"), 478 []byte(testStr + "PART-B"), 479 }) 480 481 body := &bytes.Buffer{} 482 writer := multipart.NewWriter(body) 483 484 for i := 0; i < msg.Len(); i++ { 485 part, err := writer.CreatePart(textproto.MIMEHeader{ 486 "Content-Type": []string{"application/octet-stream"}, 487 "foo-bar": []string{"baz-" + strconv.Itoa(i), "ignored"}, 488 }) 489 require.NoError(t, err) 490 491 _, err = io.Copy(part, bytes.NewReader(msg.Get(i).Get())) 492 require.NoError(t, err) 493 } 494 writer.Close() 495 496 w.Header().Add("Content-Type", writer.FormDataContentType()) 497 w.WriteHeader(http.StatusCreated) 498 w.Write(body.Bytes()) 499 })) 500 defer ts.Close() 501 502 conf := client.NewConfig() 503 conf.URL = ts.URL + "/testpost" 504 505 h, err := NewClient(conf) 506 require.NoError(t, err) 507 508 for i := 0; i < nTestLoops; i++ { 509 testStr := fmt.Sprintf("test%v", j) 510 resMsg, err := h.Send(context.Background(), nil, nil) 511 require.NoError(t, err) 512 513 assert.Equal(t, 2, resMsg.Len()) 514 assert.Equal(t, testStr+"PART-A", string(resMsg.Get(0).Get())) 515 assert.Equal(t, testStr+"PART-B", string(resMsg.Get(1).Get())) 516 assert.Equal(t, "", resMsg.Get(0).Metadata().Get("foo-bar")) 517 assert.Equal(t, "201", resMsg.Get(0).Metadata().Get("http_status_code")) 518 assert.Equal(t, "", resMsg.Get(1).Metadata().Get("foo-bar")) 519 assert.Equal(t, "201", resMsg.Get(1).Metadata().Get("http_status_code")) 520 } 521 } 522 523 func TestHTTPClientReceiveMultipartWithHeaders(t *testing.T) { 524 nTestLoops := 1000 525 526 j := 0 527 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 528 testStr := fmt.Sprintf("test%v", j) 529 j++ 530 msg := message.New([][]byte{ 531 []byte(testStr + "PART-A"), 532 []byte(testStr + "PART-B"), 533 }) 534 535 body := &bytes.Buffer{} 536 writer := multipart.NewWriter(body) 537 538 for i := 0; i < msg.Len(); i++ { 539 part, err := writer.CreatePart(textproto.MIMEHeader{ 540 "Content-Type": []string{"application/octet-stream"}, 541 "foo-bar": []string{"baz-" + strconv.Itoa(i), "ignored"}, 542 }) 543 require.NoError(t, err) 544 545 _, err = io.Copy(part, bytes.NewReader(msg.Get(i).Get())) 546 require.NoError(t, err) 547 } 548 writer.Close() 549 550 w.Header().Add("Content-Type", writer.FormDataContentType()) 551 w.WriteHeader(http.StatusCreated) 552 w.Write(body.Bytes()) 553 })) 554 defer ts.Close() 555 556 conf := client.NewConfig() 557 conf.URL = ts.URL + "/testpost" 558 conf.CopyResponseHeaders = true 559 560 h, err := NewClient(conf) 561 require.NoError(t, err) 562 563 for i := 0; i < nTestLoops; i++ { 564 testStr := fmt.Sprintf("test%v", j) 565 resMsg, err := h.Send(context.Background(), nil, nil) 566 require.NoError(t, err) 567 568 assert.Equal(t, 2, resMsg.Len()) 569 assert.Equal(t, testStr+"PART-A", string(resMsg.Get(0).Get())) 570 assert.Equal(t, testStr+"PART-B", string(resMsg.Get(1).Get())) 571 assert.Equal(t, "baz-0", resMsg.Get(0).Metadata().Get("foo-bar")) 572 assert.Equal(t, "201", resMsg.Get(0).Metadata().Get("http_status_code")) 573 assert.Equal(t, "baz-1", resMsg.Get(1).Metadata().Get("foo-bar")) 574 assert.Equal(t, "201", resMsg.Get(1).Metadata().Get("http_status_code")) 575 } 576 }