github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/http_client_test.go (about) 1 package writer 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "mime" 8 "mime/multipart" 9 "net/http" 10 "net/http/httptest" 11 "strings" 12 "sync/atomic" 13 "testing" 14 "time" 15 16 "github.com/Jeffail/benthos/v3/lib/log" 17 "github.com/Jeffail/benthos/v3/lib/message" 18 "github.com/Jeffail/benthos/v3/lib/message/roundtrip" 19 "github.com/Jeffail/benthos/v3/lib/metrics" 20 "github.com/Jeffail/benthos/v3/lib/types" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/require" 23 ) 24 25 //------------------------------------------------------------------------------ 26 27 func TestHTTPClientRetries(t *testing.T) { 28 var reqCount uint32 29 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 atomic.AddUint32(&reqCount, 1) 31 http.Error(w, "test error", http.StatusForbidden) 32 })) 33 defer ts.Close() 34 35 conf := NewHTTPClientConfig() 36 conf.URL = ts.URL + "/testpost" 37 conf.Retry = "1ms" 38 conf.NumRetries = 3 39 40 h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop()) 41 if err != nil { 42 t.Fatal(err) 43 } 44 45 if err = h.Write(message.New([][]byte{[]byte("test")})); err == nil { 46 t.Error("Expected error from end of retries") 47 } 48 49 if exp, act := uint32(4), atomic.LoadUint32(&reqCount); exp != act { 50 t.Errorf("Wrong count of HTTP attempts: %v != %v", exp, act) 51 } 52 53 h.CloseAsync() 54 if err = h.WaitForClose(time.Second); err != nil { 55 t.Error(err) 56 } 57 } 58 59 func TestHTTPClientBasic(t *testing.T) { 60 nTestLoops := 1000 61 62 resultChan := make(chan types.Message, 1) 63 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 64 msg := message.New(nil) 65 defer func() { 66 resultChan <- msg 67 }() 68 69 b, err := io.ReadAll(r.Body) 70 if err != nil { 71 t.Error(err) 72 return 73 } 74 msg.Append(message.NewPart(b)) 75 })) 76 defer ts.Close() 77 78 conf := NewHTTPClientConfig() 79 conf.URL = ts.URL + "/testpost" 80 81 h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop()) 82 if err != nil { 83 t.Fatal(err) 84 } 85 86 for i := 0; i < nTestLoops; i++ { 87 testStr := fmt.Sprintf("test%v", i) 88 testMsg := message.New([][]byte{[]byte(testStr)}) 89 90 if err = h.Write(testMsg); err != nil { 91 t.Error(err) 92 } 93 94 select { 95 case resMsg := <-resultChan: 96 if resMsg.Len() != 1 { 97 t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 1) 98 return 99 } 100 if exp, actual := testStr, string(resMsg.Get(0).Get()); exp != actual { 101 t.Errorf("Wrong result, %v != %v", exp, actual) 102 return 103 } 104 case <-time.After(time.Second): 105 t.Errorf("Action timed out") 106 return 107 } 108 } 109 110 h.CloseAsync() 111 if err = h.WaitForClose(time.Second); err != nil { 112 t.Error(err) 113 } 114 } 115 116 func TestHTTPClientSyncResponse(t *testing.T) { 117 nTestLoops := 1000 118 119 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 120 b, err := io.ReadAll(r.Body) 121 if err != nil { 122 t.Error(err) 123 return 124 } 125 w.Header().Add("fooheader", "foovalue") 126 w.Write([]byte("echo: ")) 127 w.Write(b) 128 })) 129 defer ts.Close() 130 131 conf := NewHTTPClientConfig() 132 conf.URL = ts.URL + "/testpost" 133 conf.PropagateResponse = true 134 135 h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop()) 136 if err != nil { 137 t.Fatal(err) 138 } 139 140 for i := 0; i < nTestLoops; i++ { 141 testStr := fmt.Sprintf("test%v", i) 142 143 resultStore := roundtrip.NewResultStore() 144 testMsg := message.New([][]byte{[]byte(testStr)}) 145 roundtrip.AddResultStore(testMsg, resultStore) 146 147 require.NoError(t, h.Write(testMsg)) 148 resMsgs := resultStore.Get() 149 require.Len(t, resMsgs, 1) 150 151 resMsg := resMsgs[0] 152 require.Equal(t, 1, resMsg.Len()) 153 assert.Equal(t, "echo: "+testStr, string(resMsg.Get(0).Get())) 154 assert.Equal(t, "", resMsg.Get(0).Metadata().Get("fooheader")) 155 } 156 157 h.CloseAsync() 158 if err = h.WaitForClose(time.Second); err != nil { 159 t.Error(err) 160 } 161 } 162 163 func TestHTTPClientSyncResponseCopyHeaders(t *testing.T) { 164 nTestLoops := 1000 165 166 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 167 b, err := io.ReadAll(r.Body) 168 if err != nil { 169 t.Error(err) 170 return 171 } 172 w.Header().Add("fooheader", "foovalue") 173 w.Write([]byte("echo: ")) 174 w.Write(b) 175 })) 176 defer ts.Close() 177 178 conf := NewHTTPClientConfig() 179 conf.URL = ts.URL + "/testpost" 180 conf.PropagateResponse = true 181 conf.CopyResponseHeaders = true 182 183 h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop()) 184 if err != nil { 185 t.Fatal(err) 186 } 187 188 for i := 0; i < nTestLoops; i++ { 189 testStr := fmt.Sprintf("test%v", i) 190 191 resultStore := roundtrip.NewResultStore() 192 testMsg := message.New([][]byte{[]byte(testStr)}) 193 roundtrip.AddResultStore(testMsg, resultStore) 194 195 require.NoError(t, h.Write(testMsg)) 196 resMsgs := resultStore.Get() 197 require.Len(t, resMsgs, 1) 198 199 resMsg := resMsgs[0] 200 require.Equal(t, 1, resMsg.Len()) 201 assert.Equal(t, "echo: "+testStr, string(resMsg.Get(0).Get())) 202 assert.Equal(t, "foovalue", resMsg.Get(0).Metadata().Get("fooheader")) 203 } 204 205 h.CloseAsync() 206 if err = h.WaitForClose(time.Second); err != nil { 207 t.Error(err) 208 } 209 } 210 211 func TestHTTPClientMultipart(t *testing.T) { 212 nTestLoops := 1000 213 214 resultChan := make(chan types.Message, 1) 215 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 216 msg := message.New(nil) 217 defer func() { 218 resultChan <- msg 219 }() 220 221 mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 222 if err != nil { 223 t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err) 224 return 225 } 226 227 if strings.HasPrefix(mediaType, "multipart/") { 228 mr := multipart.NewReader(r.Body, params["boundary"]) 229 for { 230 p, err := mr.NextPart() 231 if err == io.EOF { 232 break 233 } 234 if err != nil { 235 t.Error(err) 236 return 237 } 238 msgBytes, err := io.ReadAll(p) 239 if err != nil { 240 t.Error(err) 241 return 242 } 243 msg.Append(message.NewPart(msgBytes)) 244 } 245 } else { 246 b, err := io.ReadAll(r.Body) 247 if err != nil { 248 t.Error(err) 249 return 250 } 251 msg.Append(message.NewPart(b)) 252 } 253 })) 254 defer ts.Close() 255 256 conf := NewHTTPClientConfig() 257 conf.URL = ts.URL + "/testpost" 258 259 h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop()) 260 if err != nil { 261 t.Fatal(err) 262 } 263 264 for i := 0; i < nTestLoops; i++ { 265 testStr := fmt.Sprintf("test%v", i) 266 testMsg := message.New([][]byte{ 267 []byte(testStr + "PART-A"), 268 []byte(testStr + "PART-B"), 269 }) 270 271 if err = h.Write(testMsg); err != nil { 272 t.Error(err) 273 } 274 275 select { 276 case resMsg := <-resultChan: 277 if resMsg.Len() != 2 { 278 t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2) 279 return 280 } 281 if exp, actual := testStr+"PART-A", string(resMsg.Get(0).Get()); exp != actual { 282 t.Errorf("Wrong result, %v != %v", exp, actual) 283 return 284 } 285 if exp, actual := testStr+"PART-B", string(resMsg.Get(1).Get()); exp != actual { 286 t.Errorf("Wrong result, %v != %v", exp, actual) 287 return 288 } 289 case <-time.After(time.Second): 290 t.Errorf("Action timed out") 291 return 292 } 293 } 294 295 h.CloseAsync() 296 if err = h.WaitForClose(time.Second); err != nil { 297 t.Error(err) 298 } 299 } 300 func TestHTTPOutputClientMultipartBody(t *testing.T) { 301 nTestLoops := 1000 302 resultChan := make(chan types.Message, 1) 303 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 304 msg := message.New(nil) 305 defer func() { 306 resultChan <- msg 307 }() 308 309 mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 310 if err != nil { 311 t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err) 312 return 313 } 314 315 if strings.HasPrefix(mediaType, "multipart/") { 316 mr := multipart.NewReader(r.Body, params["boundary"]) 317 for { 318 p, err := mr.NextPart() 319 320 if err == io.EOF { 321 break 322 } 323 if err != nil { 324 t.Error(err) 325 return 326 } 327 msgBytes, err := io.ReadAll(p) 328 if err != nil { 329 t.Error(err) 330 return 331 } 332 msg.Append(message.NewPart(msgBytes)) 333 } 334 } 335 })) 336 defer ts.Close() 337 338 conf := NewHTTPClientConfig() 339 conf.URL = ts.URL + "/testpost" 340 conf.Multipart = []HTTPClientMultipartExpression{ 341 { 342 ContentDisposition: `form-data; name="text"`, 343 ContentType: "text/plain", 344 Body: "PART-A"}, 345 { 346 ContentDisposition: `form-data; name="file1"; filename="a.txt"`, 347 ContentType: "text/plain", 348 Body: "PART-B"}, 349 } 350 h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop()) 351 if err != nil { 352 t.Fatal(err) 353 } 354 for i := 0; i < nTestLoops; i++ { 355 if err = h.Write(message.New([][]byte{[]byte("test")})); err != nil { 356 t.Error(err) 357 } 358 select { 359 case resMsg := <-resultChan: 360 if resMsg.Len() != len(conf.Multipart) { 361 t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2) 362 return 363 } 364 if exp, actual := "PART-A", string(resMsg.Get(0).Get()); exp != actual { 365 t.Errorf("Wrong result, %v != %v", exp, actual) 366 return 367 } 368 if exp, actual := "PART-B", string(resMsg.Get(1).Get()); exp != actual { 369 t.Errorf("Wrong result, %v != %v", exp, actual) 370 return 371 } 372 case <-time.After(time.Second): 373 t.Errorf("Action timed out") 374 return 375 } 376 } 377 378 h.CloseAsync() 379 if err = h.WaitForClose(time.Second); err != nil { 380 t.Error(err) 381 } 382 } 383 384 func TestHTTPOutputClientMultipartHeaders(t *testing.T) { 385 resultChan := make(chan types.Message, 1) 386 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 387 msg := message.New(nil) 388 defer func() { 389 resultChan <- msg 390 }() 391 392 mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 393 if err != nil { 394 t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err) 395 return 396 } 397 398 if strings.HasPrefix(mediaType, "multipart/") { 399 mr := multipart.NewReader(r.Body, params["boundary"]) 400 for { 401 p, err := mr.NextPart() 402 403 if err == io.EOF { 404 break 405 } 406 if err != nil { 407 t.Error(err) 408 return 409 } 410 a, err := json.Marshal(p.Header) 411 if err != nil { 412 t.Error(err) 413 return 414 } 415 msg.Append(message.NewPart(a)) 416 } 417 } 418 })) 419 defer ts.Close() 420 421 conf := NewHTTPClientConfig() 422 conf.URL = ts.URL + "/testpost" 423 conf.Multipart = []HTTPClientMultipartExpression{ 424 { 425 ContentDisposition: `form-data; name="text"`, 426 ContentType: "text/plain", 427 Body: "PART-A"}, 428 { 429 ContentDisposition: `form-data; name="file1"; filename="a.txt"`, 430 ContentType: "text/plain", 431 Body: "PART-B"}, 432 } 433 h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop()) 434 if err != nil { 435 t.Fatal(err) 436 } 437 if err = h.Write(message.New([][]byte{[]byte("test")})); err != nil { 438 t.Error(err) 439 } 440 select { 441 case resMsg := <-resultChan: 442 for i := range conf.Multipart { 443 if resMsg.Len() != len(conf.Multipart) { 444 t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2) 445 return 446 } 447 mp := make(map[string][]string) 448 err := json.Unmarshal(resMsg.Get(i).Get(), &mp) 449 if err != nil { 450 t.Error(err) 451 } 452 if exp, actual := conf.Multipart[i].ContentDisposition, mp["Content-Disposition"]; exp != actual[0] { 453 t.Errorf("Wrong result, %v != %v", exp, actual) 454 return 455 } 456 if exp, actual := conf.Multipart[i].ContentType, mp["Content-Type"]; exp != actual[0] { 457 t.Errorf("Wrong result, %v != %v", exp, actual) 458 return 459 } 460 } 461 case <-time.After(time.Second): 462 t.Errorf("Action timed out") 463 return 464 465 } 466 h.CloseAsync() 467 if err = h.WaitForClose(time.Second); err != nil { 468 t.Error(err) 469 } 470 }