diff --git a/config/configload/load.go b/config/configload/load.go index e86905237..017cd08e1 100644 --- a/config/configload/load.go +++ b/config/configload/load.go @@ -118,6 +118,12 @@ func LoadConfig(body hcl.Body, src []byte) (*config.CouperFile, error) { file.Server = append(file.Server, srv) + serverBodies, err := mergeBackendBodies(backends, srv) + if err != nil { + return nil, err + } + srv.Remain = MergeBodies(serverBodies) + // api block(s) for _, apiBlock := range []*config.Api{srv.API} { if apiBlock == nil { @@ -129,6 +135,8 @@ func LoadConfig(body hcl.Body, src []byte) (*config.CouperFile, error) { return nil, err } + bodies = appendUniqueBodies(serverBodies, bodies...) + // empty bodies would be removed with a hcl.Merge.. later on. if err = refineEndpoints(backends, bodies, apiBlock.Endpoints); err != nil { return nil, err @@ -136,10 +144,9 @@ func LoadConfig(body hcl.Body, src []byte) (*config.CouperFile, error) { } // standalone endpoints - // TODO: free endpoints - //if err := refineEndpoints(file.Definitions, nil, srv.Endpoints); err != nil { - // return nil, err - //} + if err := refineEndpoints(backends, nil, srv.Endpoints); err != nil { + return nil, err + } } if len(file.Server) == 0 { @@ -172,7 +179,7 @@ func mergeBackendBodies(backendList Backends, inlineBackend config.Inline) ([]hc return nil, fmt.Errorf("configuration error: inlineBackend reference and inline definition") } // we have a reference, append to list and... - bodies = append(bodies, reference) + bodies = appendUniqueBodies(bodies, reference) } // ...additionally add the inline overrides. if content != nil && len(content.Attributes) > 0 { @@ -198,7 +205,7 @@ func mergeBackendBodies(backendList Backends, inlineBackend config.Inline) ([]hc bodies = append([]hcl.Body{ref}, bodies...) } - bodies = append(bodies, backends[0].Body) + bodies = appendUniqueBodies(bodies, backends[0].Body) } return bodies, nil diff --git a/config/runtime/server.go b/config/runtime/server.go index 2dcc4e550..ccf4791ef 100644 --- a/config/runtime/server.go +++ b/config/runtime/server.go @@ -48,10 +48,13 @@ type HandlerKind uint8 const ( KindAPI HandlerKind = iota + KindEndpoint KindFiles KindSPA ) +type endpointList map[*config.Endpoint]HandlerKind + // NewServerConfiguration sets http handler specific defaults and validates the given gateway configuration. // Wire up all endpoints and maps them within the returned Server. func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerConfiguration, error) { @@ -125,59 +128,85 @@ func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerC } } - if srvConf.API != nil { - // map backends to endpoint - endpoints := make(map[string]bool) + endpointsPatterns := make(map[string]bool) + + for endpoint, epType := range getEndpointsList(srvConf) { + var basePath string + var cors *config.CORS + var errTpl *errors.Template + + switch epType { + case KindAPI: + basePath = serverOptions.APIBasePath + cors = srvConf.API.CORS + errTpl = serverOptions.APIErrTpl + case KindEndpoint: + basePath = serverOptions.SrvBasePath + errTpl = serverOptions.ServerErrTpl + } - for _, endpoint := range srvConf.API.Endpoints { - pattern := utils.JoinPath("/", serverOptions.APIBasePath, endpoint.Pattern) + pattern := utils.JoinPath(basePath, endpoint.Pattern) + unique, cleanPattern := isUnique(endpointsPatterns, pattern) + if !unique { + return nil, fmt.Errorf("%s: duplicate endpoint: '%s'", endpoint.Body().MissingItemRange().String(), pattern) + } + endpointsPatterns[cleanPattern] = true - unique, cleanPattern := isUnique(endpoints, pattern) - if !unique { - return nil, fmt.Errorf("duplicate endpoint: %q", pattern) - } - endpoints[cleanPattern] = true - - // setACHandlerFn individual wrap for access_control configuration per endpoint - setACHandlerFn := func(protectedHandler http.Handler) { - api[endpoint] = configureProtectedHandler(accessControls, serverOptions.APIErrTpl, - config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl). - Merge(config.NewAccessControl(srvConf.API.AccessControl, srvConf.API.DisableAccessControl)), - config.NewAccessControl(endpoint.AccessControl, endpoint.DisableAccessControl), - protectedHandler) + // setACHandlerFn individual wrap for access_control configuration per endpoint + setACHandlerFn := func(protectedHandler http.Handler) { + ac := config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl) + if epType == KindAPI { + ac = ac.Merge(config.NewAccessControl(srvConf.API.AccessControl, srvConf.API.DisableAccessControl)) } - backendConf := *DefaultBackendConf - if diags := gohcl.DecodeBody(endpoint.Remain, confCtx, &backendConf); diags.HasErrors() { - return nil, diags - } + api[endpoint] = configureProtectedHandler(accessControls, serverOptions.APIErrTpl, + ac, + config.NewAccessControl(endpoint.AccessControl, endpoint.DisableAccessControl), + protectedHandler) + } - backend, err := newProxy(confCtx, &backendConf, srvConf.API.CORS, log, serverOptions, conf.Settings.NoProxyFromEnv) - if err != nil { - return nil, err - } + backendConf := *DefaultBackendConf + if diags := gohcl.DecodeBody(endpoint.Remain, confCtx, &backendConf); diags.HasErrors() { + return nil, diags + } - setACHandlerFn(backend) + backend, err := newProxy( + confCtx, &backendConf, cors, log, serverOptions, + conf.Settings.NoProxyFromEnv, errTpl, epType, + ) + if err != nil { + return nil, err + } - err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI) - if err != nil { - return nil, err - } + setACHandlerFn(backend) + + err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI) + if err != nil { + return nil, err } } } + return serverConfiguration, nil } func newProxy( - ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.CORS, - log *logrus.Entry, srvOpts *server.Options, noProxyFromEnv bool) (http.Handler, error) { + ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.CORS, log *logrus.Entry, + srvOpts *server.Options, noProxyFromEnv bool, errTpl *errors.Template, epType HandlerKind) (http.Handler, error) { corsOptions, err := handler.NewCORSOptions(corsOpts) if err != nil { return nil, err } - proxyOptions, err := handler.NewProxyOptions(beConf, corsOptions, noProxyFromEnv) + var kind string + switch epType { + case KindAPI: + kind = "api" + case KindEndpoint: + kind = "endpoint" + } + + proxyOptions, err := handler.NewProxyOptions(beConf, corsOptions, noProxyFromEnv, errTpl, kind) if err != nil { return nil, err } @@ -314,6 +343,8 @@ func setRoutesFromHosts(srvConf ServerConfiguration, defaultPort int, hosts []st switch kind { case KindAPI: + fallthrough + case KindEndpoint: routes = srvConf[listenPort].EndpointRoutes case KindFiles: routes = srvConf[listenPort].FileRoutes @@ -330,3 +361,19 @@ func setRoutesFromHosts(srvConf ServerConfiguration, defaultPort int, hosts []st } return nil } + +func getEndpointsList(srvConf *config.Server) endpointList { + endpoints := make(endpointList) + + if srvConf.API != nil { + for _, endpoint := range srvConf.API.Endpoints { + endpoints[endpoint] = KindAPI + } + } + + for _, endpoint := range srvConf.Endpoints { + endpoints[endpoint] = KindEndpoint + } + + return endpoints +} diff --git a/config/runtime/server/options.go b/config/runtime/server/options.go index 697472c59..b4331de2b 100644 --- a/config/runtime/server/options.go +++ b/config/runtime/server/options.go @@ -19,6 +19,7 @@ type Options struct { APIBasePath string FileBasePath string SPABasePath string + SrvBasePath string ServerName string } @@ -33,6 +34,7 @@ func NewServerOptions(conf *config.Server) (*Options, error) { return options, nil } options.ServerName = conf.Name + options.SrvBasePath = path.Join("/", conf.BasePath) if conf.ErrorFile != "" { tpl, err := errors.NewTemplateFromFile(conf.ErrorFile) @@ -44,7 +46,7 @@ func NewServerOptions(conf *config.Server) (*Options, error) { } if conf.API != nil { - options.APIBasePath = path.Join("/", conf.BasePath, conf.API.BasePath) + options.APIBasePath = path.Join(options.SrvBasePath, conf.API.BasePath) if conf.API.ErrorFile != "" { tpl, err := errors.NewTemplateFromFile(conf.API.ErrorFile) @@ -64,11 +66,11 @@ func NewServerOptions(conf *config.Server) (*Options, error) { options.FileErrTpl = tpl } - options.FileBasePath = utils.JoinPath("/", conf.BasePath, conf.Files.BasePath) + options.FileBasePath = utils.JoinPath(options.SrvBasePath, conf.Files.BasePath) } if conf.Spa != nil { - options.SPABasePath = utils.JoinPath("/", conf.BasePath, conf.Spa.BasePath) + options.SPABasePath = utils.JoinPath(options.SrvBasePath, conf.Spa.BasePath) } return options, nil diff --git a/config/runtime/server_internal_test.go b/config/runtime/server_internal_test.go index ce2c25903..02eed1f9a 100644 --- a/config/runtime/server_internal_test.go +++ b/config/runtime/server_internal_test.go @@ -5,6 +5,9 @@ import ( "testing" "github.com/avenga/couper/config" + "github.com/avenga/couper/internal/seetie" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcltest" ) func TestServer_isUnique(t *testing.T) { @@ -165,3 +168,54 @@ func TestServer_splitWildcardHostPort(t *testing.T) { t.Errorf("Expected NIL, given %s", err) } } + +func TestServer_getEndpointsList(t *testing.T) { + getHCLBody := func(in string) hcl.Body { + return hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "path": hcltest.MockExprLiteral(seetie.GoToValue(in)), + }), + }) + } + + srvConf := &config.Server{ + API: &config.Api{ + Endpoints: []*config.Endpoint{ + {Remain: getHCLBody("/api/1")}, + {Remain: getHCLBody("/api/2")}, + }, + }, + Endpoints: []*config.Endpoint{ + {Remain: getHCLBody("/free/1")}, + {Remain: getHCLBody("/free/2")}, + }, + } + + endpoints := getEndpointsList(srvConf) + if l := len(endpoints); l != 4 { + t.Fatalf("Expected 4 endpointes, given %d", l) + } + + checks := map[string]HandlerKind{ + "/api/1": KindAPI, + "/api/2": KindAPI, + "/free/1": KindEndpoint, + "/free/2": KindEndpoint, + } + + for e, kind := range endpoints { + a, _ := e.Remain.JustAttributes() + v, _ := a["path"].Expr.Value(nil) + path := seetie.ValueToString(v) + + if v, ok := checks[path]; !ok || v != kind { + t.Fatalf("Missing an endpoint for %s", path) + } + + delete(checks, path) + } + + if l := len(checks); l != 0 { + t.Fatalf("Expected 0 checks, given %d", l) + } +} diff --git a/config/server.go b/config/server.go index aaa08a4cb..157e9100c 100644 --- a/config/server.go +++ b/config/server.go @@ -1,13 +1,51 @@ package config +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" +) + +var _ Inline = &Server{} + +// Server represents the HCL block. type Server struct { - AccessControl []string `hcl:"access_control,optional"` - DisableAccessControl []string `hcl:"disable_access_control,optional"` - API *Api `hcl:"api,block"` - BasePath string `hcl:"base_path,optional"` - ErrorFile string `hcl:"error_file,optional"` - Files *Files `hcl:"files,block"` - Hosts []string `hcl:"hosts,optional"` - Name string `hcl:"name,label"` - Spa *Spa `hcl:"spa,block"` + AccessControl []string `hcl:"access_control,optional"` + DisableAccessControl []string `hcl:"disable_access_control,optional"` + API *Api `hcl:"api,block"` + Backend string `hcl:"backend,optional"` + BasePath string `hcl:"base_path,optional"` + Endpoints Endpoints `hcl:"endpoint,block"` + ErrorFile string `hcl:"error_file,optional"` + Files *Files `hcl:"files,block"` + Hosts []string `hcl:"hosts,optional"` + Name string `hcl:"name,label"` + Remain hcl.Body `hcl:",remain"` + Spa *Spa `hcl:"spa,block"` +} + +func (s Server) Body() hcl.Body { + return s.Remain +} + +func (s Server) Reference() string { + return s.Backend +} + +func (s Server) Schema(inline bool) *hcl.BodySchema { + if !inline { + schema, _ := gohcl.ImpliedBodySchema(s) + return schema + } + + type Inline struct { + Backend *Backend `hcl:"backend,block"` + } + schema, _ := gohcl.ImpliedBodySchema(&Inline{}) + + // The Server contains a backend reference, backend block is not allowed. + if s.Backend != "" { + schema.Blocks = nil + } + + return newBackendSchema(schema, s.Body()) } diff --git a/docs/README.md b/docs/README.md index bbc8aa3fb..c4b0159db 100644 --- a/docs/README.md +++ b/docs/README.md @@ -274,7 +274,7 @@ Endpoints define the entry points of Couper. The mandatory *label* defines the p | Name | Description | |:-------------------|:--------------------------------------| -|context|`api` block| +|context|`server` and `api` block| |*label*|| | `path`|| |[**`access_control`**](#access_control_attribute)|sets predefined `access_control` for `endpoint`| diff --git a/errors/code.go b/errors/code.go index 8ee9f525a..3556a5c21 100644 --- a/errors/code.go +++ b/errors/code.go @@ -38,6 +38,13 @@ const ( UpstreamResponseBufferingFailed ) +const ( + EndpointError Code = 7000 + iota + EndpointConnect + EndpointProxyConnect + EndpointReqBodySizeExceeded +) + var codes = map[Code]string{ // 1xxx Server: "Server error", diff --git a/handler/openapi_validator_test.go b/handler/openapi_validator_test.go index cf542db36..ce516a99f 100644 --- a/handler/openapi_validator_test.go +++ b/handler/openapi_validator_test.go @@ -55,13 +55,13 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) { Expr: hcltest.MockExprLiteral(cty.StringVal(origin.URL)), }, }}), - OpenAPI: []*config.OpenAPI{&config.OpenAPI{ + OpenAPI: []*config.OpenAPI{{ File: filepath.Join("testdata/validation/backend_01_openapi.yaml"), }}, RequestBodyLimit: "64MiB", } - proxyOpts, err := handler.NewProxyOptions(beConf, &handler.CORSOptions{}, config.DefaultSettings.NoProxyFromEnv) + proxyOpts, err := handler.NewProxyOptions(beConf, &handler.CORSOptions{}, config.DefaultSettings.NoProxyFromEnv, errors.DefaultJSON, "api") helper.Must(err) backend, err := handler.NewProxy(proxyOpts, log.WithContext(context.Background()), &server.Options{APIErrTpl: errors.DefaultJSON}, eval.NewENVContext(nil)) diff --git a/handler/proxy.go b/handler/proxy.go index 9d3f586b6..e155f659f 100644 --- a/handler/proxy.go +++ b/handler/proxy.go @@ -213,7 +213,7 @@ func (p *Proxy) roundtrip(rw http.ResponseWriter, req *http.Request) { err := p.Director(outreq) if err != nil { roundtripInfo.Err = err - p.srvOptions.APIErrTpl.ServeError(err).ServeHTTP(rw, req) + p.options.ErrorTemplate.ServeError(err).ServeHTTP(rw, req) return } @@ -250,7 +250,7 @@ func (p *Proxy) roundtrip(rw http.ResponseWriter, req *http.Request) { if p.options.OpenAPI != nil { apiValidator = NewOpenAPIValidator(p.options.OpenAPI) if roundtripInfo.Err = apiValidator.ValidateRequest(outreq, roundtripInfo); roundtripInfo.Err != nil { - p.srvOptions.APIErrTpl.ServeError(couperErr.UpstreamRequestValidationFailed).ServeHTTP(rw, req) + p.options.ErrorTemplate.ServeError(couperErr.UpstreamRequestValidationFailed).ServeHTTP(rw, req) return } } @@ -263,7 +263,7 @@ func (p *Proxy) roundtrip(rw http.ResponseWriter, req *http.Request) { if strings.HasPrefix(err.Error(), "proxyconnect") { errCode = couperErr.APIProxyConnect } - p.srvOptions.APIErrTpl.ServeError(errCode).ServeHTTP(rw, req) + p.options.ErrorTemplate.ServeError(p.getErrorCode(errCode)).ServeHTTP(rw, req) return } @@ -297,7 +297,7 @@ func (p *Proxy) roundtrip(rw http.ResponseWriter, req *http.Request) { if apiValidator != nil { roundtripInfo.Err = apiValidator.ValidateResponse(res, roundtripInfo) if roundtripInfo.Err != nil { - p.srvOptions.APIErrTpl.ServeError(couperErr.UpstreamResponseValidationFailed).ServeHTTP(rw, req) + p.options.ErrorTemplate.ServeError(couperErr.UpstreamResponseValidationFailed).ServeHTTP(rw, req) return } } @@ -570,7 +570,7 @@ func (p *Proxy) SetGetBody(req *http.Request) error { } if n > p.options.RequestBodyLimit { - return couperErr.APIReqBodySizeExceeded + return p.getErrorCode(couperErr.APIReqBodySizeExceeded) } bodyBytes := buf.Bytes() @@ -744,7 +744,24 @@ func (p *Proxy) Options() *server.Options { } func (p *Proxy) String() string { - return "api" + return p.options.Kind +} + +func (p *Proxy) getErrorCode(code couperErr.Code) couperErr.Code { + if p.options.Kind == "endpoint" { + switch code { + case couperErr.APIConnect: + return couperErr.EndpointConnect + case couperErr.APIProxyConnect: + return couperErr.EndpointProxyConnect + case couperErr.APIReqBodySizeExceeded: + return couperErr.EndpointReqBodySizeExceeded + } + + return couperErr.EndpointError + } + + return code } func setHeaderFields(header http.Header, options OptionsMap) { diff --git a/handler/proxy_options.go b/handler/proxy_options.go index d6bcc4e64..d85318034 100644 --- a/handler/proxy_options.go +++ b/handler/proxy_options.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/avenga/couper/config" + "github.com/avenga/couper/errors" ) type ProxyOptions struct { @@ -20,10 +21,15 @@ type ProxyOptions struct { DisableCertValidation bool MaxConnections int OpenAPI *OpenAPIValidatorOptions + ErrorTemplate *errors.Template + Kind string RequestBodyLimit int64 } -func NewProxyOptions(conf *config.Backend, corsOpts *CORSOptions, noProxyFromEnv bool) (*ProxyOptions, error) { +func NewProxyOptions( + conf *config.Backend, corsOpts *CORSOptions, noProxyFromEnv bool, + errTpl *errors.Template, kind string, +) (*ProxyOptions, error) { var timeout, connTimeout, ttfbTimeout time.Duration if err := parseDuration(conf.Timeout, &timeout); err != nil { return nil, err @@ -59,6 +65,8 @@ func NewProxyOptions(conf *config.Backend, corsOpts *CORSOptions, noProxyFromEnv MaxConnections: conf.MaxConnections, NoProxyFromEnv: noProxyFromEnv, OpenAPI: openAPIValidatorOptions, + ErrorTemplate: errTpl, + Kind: kind, RequestBodyLimit: bodyLimit, TTFBTimeout: ttfbTimeout, Timeout: timeout, diff --git a/handler/proxy_test.go b/handler/proxy_test.go index f4f0db947..03ff4efd1 100644 --- a/handler/proxy_test.go +++ b/handler/proxy_test.go @@ -40,16 +40,18 @@ func TestProxy_ServeHTTP_Timings(t *testing.T) { })) defer origin.Close() + tpl, _ := errors.NewTemplate("text/plain", []byte("error")) + tests := []struct { name string options *handler.ProxyOptions req *http.Request expectedStatus int }{ - {"with zero timings", &handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL)}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusNoContent}, - {"with overall timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", "http://1.2.3.4/"), Timeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.5/", nil), http.StatusBadGateway}, - {"with connect timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", "http://blackhole.webpagetest.org/"), ConnectTimeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.6/", nil), http.StatusBadGateway}, - {"with ttfb timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL), TTFBTimeout: time.Second}, httptest.NewRequest(http.MethodHead, "http://1.2.3.7/", nil), http.StatusBadGateway}, + {"with zero timings", &handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL), ErrorTemplate: tpl}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusNoContent}, + {"with overall timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", "http://1.2.3.4/"), ErrorTemplate: tpl, Timeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.5/", nil), http.StatusBadGateway}, + {"with connect timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", "http://blackhole.webpagetest.org/"), ErrorTemplate: tpl, ConnectTimeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.6/", nil), http.StatusBadGateway}, + {"with ttfb timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL), ErrorTemplate: tpl, TTFBTimeout: time.Second}, httptest.NewRequest(http.MethodHead, "http://1.2.3.7/", nil), http.StatusBadGateway}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -462,6 +464,8 @@ func TestProxy_ServeHTTP_Validation(t *testing.T) { logger, hook := logrustest.NewNullLogger() + tpl, _ := errors.NewTemplate("text/plain", []byte("error")) + for _, tt := range tests { t.Run(tt.name, func(subT *testing.T) { @@ -472,7 +476,7 @@ func TestProxy_ServeHTTP_Validation(t *testing.T) { content := helper.NewProxyContext(` origin = "` + origin.URL + `" `) - p, err := handler.NewProxy(&handler.ProxyOptions{Context: content, OpenAPI: openapiValidatorOptions}, logger.WithContext(context.Background()), srvOpts, eval.NewENVContext(nil)) + p, err := handler.NewProxy(&handler.ProxyOptions{Context: content, OpenAPI: openapiValidatorOptions, ErrorTemplate: tpl}, logger.WithContext(context.Background()), srvOpts, eval.NewENVContext(nil)) if err != nil { subT.Fatal(err) } @@ -712,7 +716,7 @@ func TestProxy_SetGetBody_LimitBody_Roundtrip(t *testing.T) { proxyOpts, err := handler.NewProxyOptions(&config.Backend{ Remain: helper.NewProxyContext("set_request_headers = { x = req.post }"), // ensure buffering is enabled RequestBodyLimit: testcase.limit, - }, nil, config.DefaultSettings.NoProxyFromEnv) + }, nil, config.DefaultSettings.NoProxyFromEnv, nil, "api") if err != nil { subT.Error(err) return diff --git a/server/http_integration_test.go b/server/http_integration_test.go index 4e0884370..8bd35414d 100644 --- a/server/http_integration_test.go +++ b/server/http_integration_test.go @@ -628,6 +628,12 @@ func TestHTTPServer_QueryParams(t *testing.T) { }, Path: "/xxx", }}, + {"09_couper.hcl", "", expectation{ + Query: url.Values{ + "test": []string{"pest"}, + }, + Path: "/", + }}, } { shutdown, _ := newCouper(path.Join(confPath, tc.file), test.New(t)) @@ -797,6 +803,28 @@ func TestHTTPServer_QueryEncoding(t *testing.T) { shutdown() } +func TestHTTPServer_Backends(t *testing.T) { + client := newClient() + + config := "testdata/integration/config/02_couper.hcl" + + shutdown, _ := newCouper(config, test.New(t)) + defer shutdown() + + helper := test.New(t) + + req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil) + helper.Must(err) + + res, err := client.Do(req) + helper.Must(err) + + exp := []string{"1", "2", "3", "4"} + if !reflect.DeepEqual(res.Header.Values("Foo"), exp) { + t.Errorf("\nwant: \n%#v\ngot: \n%#v", exp, res.Header.Values("Foo")) + } +} + func TestHTTPServer_TrailingSlash(t *testing.T) { client := newClient() @@ -1038,7 +1066,7 @@ func TestConfigBodyContentBackends(t *testing.T) { } for _, tc := range []testCase{ - {"/anything", http.Header{"Foo": []string{"3"}}, url.Values{"bar": []string{"3", "4"}}}, + {"/anything", http.Header{"Foo": []string{"4"}}, url.Values{"bar": []string{"3", "4"}}}, {"/get", http.Header{"Foo": []string{"1", "3"}}, url.Values{"bar": []string{"1", "3", "4"}}}, } { t.Run(tc.path[1:], func(subT *testing.T) { diff --git a/server/testdata/integration/config/02_couper.hcl b/server/testdata/integration/config/02_couper.hcl index 84fc7b252..069663f01 100644 --- a/server/testdata/integration/config/02_couper.hcl +++ b/server/testdata/integration/config/02_couper.hcl @@ -1,9 +1,16 @@ server "backends" { + + backend "b" { + add_response_headers = { + foo = "2" + } + } + api { backend "b" { origin = env.COUPER_TEST_BACKEND_ADDR add_response_headers = { - foo = "2" + foo = "3" } add_query_params = { bar = "2" @@ -17,7 +24,7 @@ server "backends" { backend { origin = env.COUPER_TEST_BACKEND_ADDR add_response_headers = { - foo = "3" + foo = "4" } add_query_params = { bar = "4" @@ -25,6 +32,14 @@ server "backends" { } } + endpoint "/" { + backend "b" { + add_response_headers = { + foo = "4" + } + } + } + endpoint "/get" { add_query_params = { bar = "3" diff --git a/server/testdata/integration/endpoint_eval/09_couper.hcl b/server/testdata/integration/endpoint_eval/09_couper.hcl new file mode 100644 index 000000000..858e31242 --- /dev/null +++ b/server/testdata/integration/endpoint_eval/09_couper.hcl @@ -0,0 +1,11 @@ +server "free-endpoint" { + endpoint "/" { + backend { + set_query_params = { + test = "pest" + } + + origin = env.COUPER_TEST_BACKEND_ADDR + } + } +}