diff --git a/core/requestFnURL.go b/core/requestFnURL.go new file mode 100644 index 0000000..8573a23 --- /dev/null +++ b/core/requestFnURL.go @@ -0,0 +1,169 @@ +// Package core provides utility methods that help convert ALB events +// into an http.Request and http.ResponseWriter +package core + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambdacontext" +) + +const ( + // FnURLContextHeader is the custom header key used to store the + // Function URL context. To access the Context properties use the + // GetContext method of the RequestAccessorFnURL object. + FnURLContextHeader = "X-GoLambdaProxy-FnURL-Context" +) + +// RequestAccessorFnURL objects give access to custom Function URL properties +// in the request. +type RequestAccessorFnURL struct { + stripBasePath string +} + +// GetALBContext extracts the ALB context object from a request's custom header. +// Returns a populated events.ALBTargetGroupRequestContext object from the request. +func (r *RequestAccessorFnURL) GetContext(req *http.Request) (events.LambdaFunctionURLRequestContext, error) { + if req.Header.Get(FnURLContextHeader) == "" { + return events.LambdaFunctionURLRequestContext{}, errors.New("no context header in request") + } + context := events.LambdaFunctionURLRequestContext{} + err := json.Unmarshal([]byte(req.Header.Get(FnURLContextHeader)), &context) + if err != nil { + log.Println("Error while unmarshalling context") + log.Println(err) + return events.LambdaFunctionURLRequestContext{}, err + } + return context, nil +} + +// StripBasePath instructs the RequestAccessor object that the given base +// path should be removed from the request path before sending it to the +// framework for routing. This is used when API Gateway is configured with +// base path mappings in custom domain names. +func (r *RequestAccessorFnURL) StripBasePath(basePath string) string { + if strings.Trim(basePath, " ") == "" { + r.stripBasePath = "" + return "" + } + + newBasePath := basePath + if !strings.HasPrefix(newBasePath, "/") { + newBasePath = "/" + newBasePath + } + + if strings.HasSuffix(newBasePath, "/") { + newBasePath = newBasePath[:len(newBasePath)-1] + } + + r.stripBasePath = newBasePath + + return newBasePath +} + +// FunctionURLEventToHTTPRequest converts an a Function URL event into a http.Request object. +// Returns the populated http request with additional custom header for the Function URL context. +// To access these properties use the GetContext method of the RequestAccessorFnURL object. +func (r *RequestAccessorFnURL) FunctionURLEventToHTTPRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToHeaderFnURL(httpRequest, req) +} + +// FunctionURLEventToHTTPRequestWithContext converts a Function URL event and context into an http.Request object. +// Returns the populated http request with lambda context, Function URL RequestContext as part of its context. +func (r *RequestAccessorFnURL) FunctionURLEventToHTTPRequestWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (*http.Request, error) { + httpRequest, err := r.EventToRequest(req) + if err != nil { + log.Println(err) + return nil, err + } + return addToContextFnURL(ctx, httpRequest, req), nil +} + +// EventToRequest converts a Function URL event into an http.Request object. +// Returns the populated request maintaining headers +func (r *RequestAccessorFnURL) EventToRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) { + decodedBody := []byte(req.Body) + if req.IsBase64Encoded { + base64Body, err := base64.StdEncoding.DecodeString(req.Body) + if err != nil { + return nil, err + } + decodedBody = base64Body + } + + path := req.RawPath + if r.stripBasePath != "" && len(r.stripBasePath) > 1 { + if strings.HasPrefix(path, r.stripBasePath) { + path = strings.Replace(path, r.stripBasePath, "", 1) + } + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + serverAddress := "https://" + req.RequestContext.DomainName + if customAddress, ok := os.LookupEnv(CustomHostVariable); ok { + serverAddress = customAddress + } + + path = serverAddress + path + "?" + req.RawQueryString + + httpRequest, err := http.NewRequest( + strings.ToUpper(req.RequestContext.HTTP.Method), + path, + bytes.NewReader(decodedBody), + ) + + if err != nil { + fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RawPath) + log.Println(err) + return nil, err + } + + for header, val := range req.Headers { + httpRequest.Header.Add(header, val) + } + + httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP + httpRequest.RequestURI = httpRequest.URL.RequestURI() + + return httpRequest, nil +} + +func addToHeaderFnURL(req *http.Request, fnUrlRequest events.LambdaFunctionURLRequest) (*http.Request, error) { + ctx, err := json.Marshal(fnUrlRequest.RequestContext) + if err != nil { + log.Println("Could not Marshal Function URL context for custom header") + return req, err + } + req.Header.Set(FnURLContextHeader, string(ctx)) + return req, nil +} + +// adds context data to http request so we can pass +func addToContextFnURL(ctx context.Context, req *http.Request, fnUrlRequest events.LambdaFunctionURLRequest) *http.Request { + lc, _ := lambdacontext.FromContext(ctx) + rc := requestContextFnURL{lambdaContext: lc, fnUrlContext: fnUrlRequest.RequestContext} + ctx = context.WithValue(ctx, ctxKey{}, rc) + return req.WithContext(ctx) +} + +type requestContextFnURL struct { + lambdaContext *lambdacontext.LambdaContext + fnUrlContext events.LambdaFunctionURLRequestContext +} diff --git a/core/responseFnURL.go b/core/responseFnURL.go new file mode 100644 index 0000000..1682681 --- /dev/null +++ b/core/responseFnURL.go @@ -0,0 +1,117 @@ +// Package core provides utility methods that help convert proxy events +// into an http.Request and http.ResponseWriter +package core + +import ( + "bytes" + "encoding/base64" + "errors" + "net/http" + "unicode/utf8" + + "github.com/aws/aws-lambda-go/events" +) + +// ProxyResponseWriterFunctionURL implements http.ResponseWriter and adds the method +// necessary to return an events.LambdaFunctionURLResponse object +type ProxyResponseWriterFunctionURL struct { + status int + headers http.Header + body bytes.Buffer + observers []chan<- bool +} + +// Ensure implementation satisfies http.ResponseWriter interface +var ( + _ http.ResponseWriter = &ProxyResponseWriterFunctionURL{} +) + +// NewProxyResponseWriterFnURL returns a new ProxyResponseWriterFunctionURL object. +// The object is initialized with an empty map of headers and a status code of -1 +func NewProxyResponseWriterFnURL() *ProxyResponseWriterFunctionURL { + return &ProxyResponseWriterFunctionURL{ + headers: make(http.Header), + status: defaultStatusCode, + observers: make([]chan<- bool, 0), + } +} + +func (r *ProxyResponseWriterFunctionURL) CloseNotify() <-chan bool { + ch := make(chan bool, 1) + + r.observers = append(r.observers, ch) + + return ch +} + +func (r *ProxyResponseWriterFunctionURL) notifyClosed() { + for _, v := range r.observers { + v <- true + } +} + +// Header implementation from the http.ResponseWriter interface. +func (r *ProxyResponseWriterFunctionURL) Header() http.Header { + return r.headers +} + +// Write sets the response body in the object. If no status code +// was set before with the WriteHeader method it sets the status +// for the response to 200 OK. +func (r *ProxyResponseWriterFunctionURL) Write(body []byte) (int, error) { + if r.status == defaultStatusCode { + r.status = http.StatusOK + } + + // if the content type header is not set when we write the body we try to + // detect one and set it by default. If the content type cannot be detected + // it is automatically set to "application/octet-stream" by the + // DetectContentType method + if r.Header().Get(contentTypeHeaderKey) == "" { + r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body)) + } + + return (&r.body).Write(body) +} + +// WriteHeader sets a status code for the response. This method is used +// for error responses. +func (r *ProxyResponseWriterFunctionURL) WriteHeader(status int) { + r.status = status +} + +// GetProxyResponse converts the data passed to the response writer into +// an events.LambdaFunctionURLResponse object. +// Returns a populated proxy response object. If the response is invalid, for example +// has no headers or an invalid status code returns an error. +func (r *ProxyResponseWriterFunctionURL) GetProxyResponse() (events.LambdaFunctionURLResponse, error) { + r.notifyClosed() + + if r.status == defaultStatusCode { + return events.LambdaFunctionURLResponse{}, errors.New("status code not set on response") + } + + var output string + isBase64 := false + + bb := (&r.body).Bytes() + + if utf8.Valid(bb) { + output = string(bb) + } else { + output = base64.StdEncoding.EncodeToString(bb) + isBase64 = true + } + + headers := make(map[string]string) + for h, v := range r.Header() { + headers[h] = v[0] + } + + return events.LambdaFunctionURLResponse{ + StatusCode: r.status, + Headers: headers, + Body: output, + IsBase64Encoded: isBase64, + }, nil +} diff --git a/core/typesFnURL.go b/core/typesFnURL.go new file mode 100644 index 0000000..f70e128 --- /dev/null +++ b/core/typesFnURL.go @@ -0,0 +1,12 @@ +package core + +import ( + "net/http" + + "github.com/aws/aws-lambda-go/events" +) + +// GatewayTimeoutFnURL returns a dafault Gateway Timeout (504) response +func GatewayTimeoutFnURL() events.LambdaFunctionURLResponse { + return events.LambdaFunctionURLResponse{StatusCode: http.StatusGatewayTimeout} +} diff --git a/go.mod b/go.mod index 56b2f61..f6e6041 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/BurntSushi/toml v1.1.0 // indirect - github.com/aws/aws-lambda-go v1.19.1 + github.com/aws/aws-lambda-go v1.41.0 github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible // indirect github.com/gin-gonic/gin v1.7.7 github.com/go-chi/chi/v5 v5.0.2 @@ -27,7 +27,6 @@ require ( golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/ini.v1 v1.66.6 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) replace ( diff --git a/go.sum b/go.sum index 01ada08..c769ad5 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-lambda-go v1.19.1 h1:5iUHbIZ2sG6Yq/J1IN3sWm3+vAB1CWwhI21NffLNuNI= -github.com/aws/aws-lambda-go v1.19.1/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= +github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= +github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -88,7 +88,6 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -430,8 +429,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tdewolff/minify/v2 v2.10.0/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM= github.com/tdewolff/minify/v2 v2.11.10 h1:2tk9nuKfc8YOTD8glZ7JF/VtE8W5HOgmepWdjcPtRro= @@ -448,7 +448,6 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -902,7 +901,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlerfunc/adapterFnURL.go b/handlerfunc/adapterFnURL.go new file mode 100644 index 0000000..a4dcc58 --- /dev/null +++ b/handlerfunc/adapterFnURL.go @@ -0,0 +1,13 @@ +package handlerfunc + +import ( + "net/http" + + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" +) + +type HandlerFuncAdapterFnURL = httpadapter.HandlerAdapterFnURL + +func NewFunctionURL(handlerFunc http.HandlerFunc) *HandlerFuncAdapterFnURL { + return httpadapter.NewFunctionURL(handlerFunc) +} diff --git a/handlerfunc/adapterFnURL_test.go b/handlerfunc/adapterFnURL_test.go new file mode 100644 index 0000000..5e99c11 --- /dev/null +++ b/handlerfunc/adapterFnURL_test.go @@ -0,0 +1,48 @@ +package handlerfunc_test + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("HandlerFuncAdapter tests", func() { + Context("Simple ping request", func() { + It("Proxies the event correctly", func() { + log.Println("Starting test") + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("unfortunately-required-header", "") + fmt.Fprintf(w, "Go Lambda!!") + }) + + adapter := httpadapter.NewFunctionURL(handler) + + req := events.LambdaFunctionURLRequest{ + RequestContext: events.LambdaFunctionURLRequestContext{ + HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{ + Method: http.MethodGet, + Path: "/ping", + }, + }, + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + + resp, err = adapter.Proxy(req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + }) + }) +}) diff --git a/httpadapter/adapterFnURL.go b/httpadapter/adapterFnURL.go new file mode 100644 index 0000000..9a0f511 --- /dev/null +++ b/httpadapter/adapterFnURL.go @@ -0,0 +1,52 @@ +package httpadapter + +import ( + "context" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/core" +) + +type HandlerAdapterFnURL struct { + core.RequestAccessorFnURL + handler http.Handler +} + +func NewFunctionURL(handler http.Handler) *HandlerAdapterFnURL { + return &HandlerAdapterFnURL{ + handler: handler, + } +} + +// Proxy receives an ALB Target Group proxy event, transforms it into an http.Request +// object, and sends it to the http.HandlerFunc for routing. +// It returns a proxy response object generated from the http.ResponseWriter. +func (h *HandlerAdapterFnURL) Proxy(event events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + req, err := h.FunctionURLEventToHTTPRequest(event) + return h.proxyInternal(req, err) +} + +// ProxyWithContext receives context and an ALB proxy event, +// transforms them into an http.Request object, and sends it to the http.Handler for routing. +// It returns a proxy response object generated from the http.ResponseWriter. +func (h *HandlerAdapterFnURL) ProxyWithContext(ctx context.Context, event events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { + req, err := h.FunctionURLEventToHTTPRequestWithContext(ctx, event) + return h.proxyInternal(req, err) +} + +func (h *HandlerAdapterFnURL) proxyInternal(req *http.Request, err error) (events.LambdaFunctionURLResponse, error) { + if err != nil { + return core.GatewayTimeoutFnURL(), core.NewLoggedError("Could not convert proxy event to request: %v", err) + } + + w := core.NewProxyResponseWriterFnURL() + h.handler.ServeHTTP(http.ResponseWriter(w), req) + + resp, err := w.GetProxyResponse() + if err != nil { + return core.GatewayTimeoutFnURL(), core.NewLoggedError("Error while generating proxy response: %v", err) + } + + return resp, nil +} diff --git a/httpadapter/adapterFnURL_test.go b/httpadapter/adapterFnURL_test.go new file mode 100644 index 0000000..ff13961 --- /dev/null +++ b/httpadapter/adapterFnURL_test.go @@ -0,0 +1,48 @@ +package httpadapter_test + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("HandlerFuncAdapter tests", func() { + Context("Simple ping request", func() { + It("Proxies the event correctly", func() { + log.Println("Starting test") + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("unfortunately-required-header", "") + fmt.Fprintf(w, "Go Lambda!!") + }) + + adapter := httpadapter.NewFunctionURL(handler) + + req := events.LambdaFunctionURLRequest{ + RequestContext: events.LambdaFunctionURLRequestContext{ + HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{ + Method: http.MethodGet, + Path: "/ping", + }, + }, + } + + resp, err := adapter.ProxyWithContext(context.Background(), req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + + resp, err = adapter.Proxy(req) + + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(200)) + }) + }) +})