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  }