Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unexpected_status attribute #835

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions config/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ func (p Proxy) Inline() interface{} {
meta.ResponseHeadersAttributes
meta.FormParamsAttributes
meta.QueryParamsAttributes
Backend *Backend `hcl:"backend,block" docs:"Configures a [backend](/configuration/block/backend) for the proxy request (zero or one). Mutually exclusive with {backend} attribute."`
ExpectedStatus []int `hcl:"expected_status,optional" docs:"If defined, the response status code will be verified against this list of codes. If the status code not included in this list an {unexpected_status} error will be thrown which can be handled with an [{error_handler}](error_handler)."`
URL string `hcl:"url,optional" docs:"URL of the resource to request. May be relative to an origin specified in a referenced or nested {backend} block."`
Websockets *Websockets `hcl:"websockets,block" docs:"Configures support for [websockets](/configuration/block/websockets) connections (zero or one). Mutually exclusive with {websockets} attribute."`
Backend *Backend `hcl:"backend,block" docs:"Configures a [backend](/configuration/block/backend) for the proxy request (zero or one). Mutually exclusive with {backend} attribute."`
ExpectedStatus []int `hcl:"expected_status,optional" docs:"If defined, the response status code will be verified against this list of codes. If the status code is not included in this list an {unexpected_status} error will be thrown which can be handled with an [{error_handler}](error_handler). Mutually exclusive with {unexpected_status}."`
UnexpectedStatus []int `hcl:"unexpected_status,optional" docs:"If defined, the response status code will be verified against this list of codes. If the status code is included in this list an {unexpected_status} error will be thrown which can be handled with an [{error_handler}](error_handler). Mutually exclusive with {expected_status}."`
URL string `hcl:"url,optional" docs:"URL of the resource to request. May be relative to an origin specified in a referenced or nested {backend} block."`
Websockets *Websockets `hcl:"websockets,block" docs:"Configures support for [websockets](/configuration/block/websockets) connections (zero or one). Mutually exclusive with {websockets} attribute."`
}

return &Inline{}
Expand Down
1 change: 1 addition & 0 deletions config/request/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
EndpointExpectedStatus
EndpointKind
EndpointSequenceDependsOn
EndpointUnexpectedStatus
Error
GrantedPermissions
Handler
Expand Down
10 changes: 10 additions & 0 deletions config/runtime/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ func NewEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,

var hasWSblock bool
proxyBody := proxyConf.HCLBody()
_, hasExpStatus := proxyBody.Attributes["expected_status"]
_, hasUnexpStatus := proxyBody.Attributes["unexpected_status"]
if hasExpStatus && hasUnexpStatus {
r := proxyBody.SrcRange
return nil, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "only one of expected_status and unexpected_status is allowed in a proxy block",
Subject: &r,
}}
}
for _, b := range proxyBody.Blocks {
if b.Type == "websockets" {
hasWSblock = true
Expand Down
8 changes: 7 additions & 1 deletion docs/website/content/2.configuration/4.block/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ values: [
},
{
"default": "[]",
"description": "If defined, the response status code will be verified against this list of codes. If the status code not included in this list an `unexpected_status` error will be thrown which can be handled with an [`error_handler`](error_handler).",
"description": "If defined, the response status code will be verified against this list of codes. If the status code is not included in this list an `unexpected_status` error will be thrown which can be handled with an [`error_handler`](error_handler). Mutually exclusive with `unexpected_status`.",
"name": "expected_status",
"type": "tuple (int)"
},
Expand Down Expand Up @@ -103,6 +103,12 @@ values: [
"name": "set_response_headers",
"type": "object"
},
{
"default": "[]",
"description": "If defined, the response status code will be verified against this list of codes. If the status code is included in this list an `unexpected_status` error will be thrown which can be handled with an [`error_handler`](error_handler). Mutually exclusive with `expected_status`.",
"name": "unexpected_status",
"type": "tuple (int)"
},
{
"default": "",
"description": "URL of the resource to request. May be relative to an origin specified in a referenced or nested `backend` block.",
Expand Down
114 changes: 114 additions & 0 deletions handler/producer/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package producer_test

import (
"context"
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"

"github.com/coupergateway/couper/errors"
"github.com/coupergateway/couper/eval"
"github.com/coupergateway/couper/handler"
"github.com/coupergateway/couper/handler/producer"
"github.com/coupergateway/couper/handler/transport"
"github.com/coupergateway/couper/internal/test"
)

func Test_ProxyProduceUnexpectedStatus(t *testing.T) {
origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
s, err := strconv.Atoi(req.Header.Get("X-Status"))
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
rw.WriteHeader(s)
}))
defer origin.Close()

logger, _ := test.NewLogger()
logEntry := logger.WithContext(context.Background())

backend := transport.NewBackend(&hclsyntax.Body{}, &transport.Config{Origin: origin.URL}, nil, logEntry)

clientRequest, _ := http.NewRequest(http.MethodGet, "http://couper.local", nil)

toListVal := func(numbers ...int64) cty.Value {
var list []cty.Value
for _, n := range numbers {
list = append(list, cty.NumberIntVal(n))
}
return cty.ListVal(list)
}

tests := []struct {
name string
attr *hclsyntax.Attribute
reflectStatus int // send via header, reflected by origin as http status-code
expectedErr error
}{
{"/w status /w unexpected response", &hclsyntax.Attribute{
Name: "unexpected_status",
Expr: &hclsyntax.LiteralValueExpr{Val: toListVal(200, 304)}},
http.StatusNotModified,
errors.UnexpectedStatus,
},
{"/w status /w expected response", &hclsyntax.Attribute{
Name: "unexpected_status",
Expr: &hclsyntax.LiteralValueExpr{Val: toListVal(200, 304)}},
http.StatusNotAcceptable,
nil,
},
}

for _, tt := range tests {
content := &hclsyntax.Body{Attributes: map[string]*hclsyntax.Attribute{
"url": {Name: "url", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal(origin.URL)}},
// Since request will not proxy our dynamic client-request header value, we will add a headers attr here.
// There is no validation, so this also applies to proxy (unused)
"set_request_headers": {Name: "set_request_headers", Expr: &hclsyntax.ObjectConsExpr{
Items: []hclsyntax.ObjectConsItem{
{
KeyExpr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal("X-Status")},
ValueExpr: &hclsyntax.LiteralValueExpr{Val: cty.NumberIntVal(int64(tt.reflectStatus))},
},
},
}},
}}
if tt.attr != nil {
content.Attributes[tt.attr.Name] = tt.attr
}

producers := []producer.Roundtrip{
&producer.Proxy{
Content: content,
Name: "proxy",
RoundTrip: handler.NewProxy(backend, content, false, logEntry),
},
}
testNames := []string{"request", "proxy"}

for i, rt := range producers {
t.Run(testNames[i]+"_"+tt.name, func(t *testing.T) {

ctx := eval.NewDefaultContext().WithClientRequest(clientRequest)

outreq := clientRequest.WithContext(ctx)
outreq.Header.Set("X-Status", strconv.Itoa(tt.reflectStatus))

result := rt.Produce(outreq)

if !errors.Equals(tt.expectedErr, result.Err) {
t.Fatalf("expected error: %v, got %v", tt.expectedErr, result.Err)
}

if result.Beresp == nil {
t.Fatal("expected a backend response")
}
})
}
}
}
13 changes: 13 additions & 0 deletions handler/producer/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ func roundtrip(rt http.RoundTripper, req *http.Request) *Result {
}
}

if unexpStatus, ok := req.Context().Value(request.EndpointUnexpectedStatus).([]int64); beresp != nil &&
ok && len(unexpStatus) > 0 {
for _, unexp := range unexpStatus {
if beresp.StatusCode == int(unexp) {
return &Result{
Beresp: beresp,
Err: errors.UnexpectedStatus.With(err),
RoundTripName: rtn,
}
}
}
}

if expStatus, ok := req.Context().Value(request.EndpointExpectedStatus).([]int64); beresp != nil &&
ok && len(expStatus) > 0 {
var seen bool
Expand Down
7 changes: 7 additions & 0 deletions handler/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ func (p *Proxy) RoundTrip(req *http.Request) (*http.Response, error) {

outCtx = context.WithValue(outCtx, request.EndpointExpectedStatus, seetie.ValueToIntSlice(expStatusVal))

unexpStatusVal, err := eval.ValueFromBodyAttribute(hclCtx, p.context, "unexpected_status")
if err != nil {
return nil, err
}

outCtx = context.WithValue(outCtx, request.EndpointUnexpectedStatus, seetie.ValueToIntSlice(unexpStatusVal))

*req = *req.WithContext(outCtx)

if err = p.registerWebsocketsResponse(req); err != nil {
Expand Down
1 change: 1 addition & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func Test_realmain(t *testing.T) {
{"non-string proxy reference", []string{"couper", "run", "-f", base + "/19_couper.hcl"}, nil, `level=error msg="%s/19_couper.hcl:3,13-14: proxy must evaluate to string; " build=dev`, 1},
{"proxy reference does not exist", []string{"couper", "run", "-f", base + "/20_couper.hcl"}, nil, `level=error msg="%s/20_couper.hcl:3,14-17: referenced proxy \"foo\" is not defined; " build=dev`, 1},
{"circular backend references", []string{"couper", "run", "-f", base + "/21_couper.hcl"}, nil, `level=error msg="configuration error: <nil>: configuration error; circular reference:`, 1},
{"expected and unexpected status in proxy", []string{"couper", "run", "-f", base + "/18_couper.hcl"}, nil, `level=error msg="%s/18_couper.hcl:4,13-10,8: only one of expected_status and unexpected_status is allowed in a proxy block; `, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(subT *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions server/testdata/settings/18_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
server {
api {
endpoint "/**" {
proxy {
backend {
origin = "https://example.com"
}
expected_status = [200]
unexpected_status = [401]
}
}
}
}
Loading