Skip to content

Headers transformation#76

Merged
mmacai merged 10 commits intomasterfrom
headers-transformation
May 28, 2018
Merged

Headers transformation#76
mmacai merged 10 commits intomasterfrom
headers-transformation

Conversation

@mmacai
Copy link
Copy Markdown
Collaborator

@mmacai mmacai commented May 22, 2018

  • Fixed regex matching for proxy - previously /abc/def would match against /abc/def/1

  • Fixed response headers before response send - Elixir/Plug needs lower cased headers => led to duplicate headers and wrong responses

  • Added request headers transformations via add_headers, in future we can add remove_headers, etc.

  • Unit tests and smoke tests should cover everything

Steps to test:

  • create new API at POST localhost:4010/v1/apis with
{
  "id": "new-service2",
  "name": "new-service2",
  "auth_type": "jwt",
  "auth": {
    "use_header": true,
    "header_name": "Authorization",
    "use_query": false,
    "query_name": ""
  },
  "versioned": false,
  "version_data": {
    "default": {
    	"transform_request_headers": {
          "add_headers": {
            "a": "b",
            "c": "d"
          }
        },
		"endpoints": [
			{
				"id": "get-auth-register",
				"path": "/auth/register",
				"method": "GET",
				"not_secured": true,
				"transform_request_headers": true
			}
		]
    }
  },
  "proxy": {
    "use_env": true,
    "target_url": "IS_HOST",
    "port": 6666
  }
}
  • Send request to GET localhost:4000/auth/register with header a => proxy should overwrite it and add new header b
  • Omitting "transform_request_headers": true in endpoints will ignore header transformations

@mmacai mmacai requested a review from kevinbader May 22, 2018 20:24
@kevinbader kevinbader self-assigned this May 24, 2018
@kevinbader kevinbader added this to the 2.0.0 milestone May 24, 2018
@kevinbader
Copy link
Copy Markdown
Contributor

  "versioned": false,
  "version_data": {
    "default": {

I don't really get this saying theres no versioning, but then configuring "version_data" ... I know it's done the same way in Tyk, and they probably had a reason for doing so, but it just looks very awkward to me 😅


# Transform request headers
@spec transform_req_headers(Proxy.endpoint(), Proxy.api_definition(), %Plug.Conn{}) ::
%Plug.Conn{}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, what's the difference between %Plug.Conn{} and Plug.Conn.t? I've seen the former, which refers to the module's defstruct quite often, but isn't the latter the real type we'd acutally would like to refer to? After all, the struct per se has only default values set, but no types. Might be wrong here. Any opinions?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know about Plug.Conn.t, but I think it makes sense since Strings are defined the same way.

%Plug.Conn{}
defp transform_req_headers(
%{"transform_request_headers" => true} = endpoint,
%{"versioned" => false} = api,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please give me an RTFM link to what the meaning of "versioned" is here? Cause it's not obvious to me why versioning an endpoint would affect the headers 😕

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning behind this is:

versioned: false => means you have only default API, no versions no magic logic around it, you care just about endpoints from default
versioned: true => you have multiple versions, you can't use only default, you have to take care about all versions

if you have versioned: true, you clearly have multiple versions of the same API => every version of API may require different headers transformation (thus how to transform is per version), every endpoint may or may not want do use this header transformation (thus endpoints have to decide if they want to use transformations)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but doesn't %{"transform_request_headers" => true} = endpoint say that the transformation should be enabled on this specific endpoint?

conn = %{conn | resp_headers: headers}
if Serializer.header_value?(conn, "transfer-encoding", "chunked") do
send_chunked_response(conn, headers, status_code, body)
down_cased_headers = headers |> Serializer.downcase_headers
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm it's downcase in the std lib, it's downcase in Serializer, so I'd go with downcased_headers here

send_chunked_response(conn, down_cased_headers, status_code, body)
else
%{conn | resp_headers: headers} |> send_resp(status_code, body)
%{conn | resp_headers: down_cased_headers} |> send_resp(status_code, body)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this replacing resp_headers a second time?

alias Plug.Conn.Query
alias RigInboundGateway.Proxy

@typep headers_list :: [{String.t, String.t}, ...]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer either header_list or headers.

Also, can't old_headers be empty, ever? I mean, sending a request without any headers would look more like [{String.t, String.t}]

|> Enum.find({}, fn({header_key, _}) -> header_key == key end)
|> Tuple.to_list
|> Enum.member?(value)
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm this work unexpectedly for headers_list=[{"a", "b"}], key="a", value="a", because keys and values are mixed up.

May I suggest a slightly different implementation:

def header_value?(headers, key, value) do
  headers
  |> Enum.find(fn
    {^key, ^value} -> true
    _ -> false
  end)
  |> case do
    nil -> false
    _ -> true
  end
end

Not sure about whether pattern matching makes it better or not, but the call to Tuple.to_list/1 definitely seems a bit odd here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, added test as well for it

key
|> String.downcase
key_downcase == key
{key_downcase, value}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps it would suffice to simply write

{String.downcase(key), value}

old_headers
|> Enum.filter(fn({old_key, _}) ->
new_headers
|> Enum.find(nil, fn({new_key, _}) -> old_key == new_key end)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nil is the default "default" value, no need to specify it explicitly

|> Enum.find(nil, fn({new_key, _}) -> old_key == new_key end)
|> is_nil
end)
|> Enum.concat(new_headers)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm maybe I get this wrong, but it seems this

  1. takes all old headers that don't occur in new_headers and
  2. add the new headers to that list.
    Would that be easier to read if written this way:
Map.merge(Map.new(old_headers), Map.new(new_headers))
|> Enum.to_list()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, totally overlooked it.

assert get_req_header(conn, "john") == []
assert conn.status == 200
assert conn.resp_body =~ "{\"status\":\"ok\"}"
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be nice to have a test around how it behaves with versioned set or unset

Copy link
Copy Markdown
Collaborator Author

@mmacai mmacai May 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no support for versioning yet, it would crash with ** (FunctionClauseError) no function clause matching. Once we decide to add versioning it definitely makes sense.

%Plug.Conn{}
defp transform_req_headers(
%{"transform_request_headers" => true} = endpoint,
%{"versioned" => false} = api,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but doesn't %{"transform_request_headers" => true} = endpoint say that the transformation should be enabled on this specific endpoint?

conn = %{conn | resp_headers: downcased_headers}

if Serializer.header_value?(downcased_headers, "transfer-encoding", "chunked") do
send_chunked_response(conn, downcased_headers, status_code, body)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are the headers passed here again - conn should already include them at this point

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

facepalm

alias Plug.Conn.Query
alias RigInboundGateway.Proxy

@typep headers :: [{String.t, String.t}, ...]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if old_headers is empty? I mean, sending a request without any headers would look more like [{String.t, String.t}]

@mmacai mmacai merged commit ca83939 into master May 28, 2018
@kevinbader kevinbader deleted the headers-transformation branch May 30, 2018 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants