Endpoints

Endpoints are objects that represent the server-side of http transactions. They are instances of the Endpoint class. Endpoint receive HTTP requests and return HTTP responses. The responses are generated by handlers, which can be static functions, object instance methods, or closures. Handlers can be set when the endpoint is created, or specified on a per-client-request basis. Endpoints are created using the addEndpoint method of the Deproxy class. (Instantiating an Endpoint object via the construct is discouraged.)

Endpoints are very flexible. You can create intricate testing situations with them in combination with custom handlers.

How To: Single Server

In the simplest case, a proxy sits in between the client and a server.

 ________                         ________                        ________
|        |  --->  Request  --->  |        |  ---> Request  --->  |        |
| Client |                       | Proxy  |                      | Server |
|________|  <---  Response <---  |________|  <--- Response <---  |________|

Since we’re testing the proxy, we want to be able to control the responses that the server sends in reaction to requests from the proxy. We can simulate the server using a single Endpoint. By default, the endpoint will simply return 200 OK responses, unless it is given a different handler upon creation.

Deproxy deproxy = new Deproxy()

Endpoint endpoint = deproxy.addEndpoint(9999)

def theProxy = new TheProxy(port: 8080,
                            targetHostname: "localhost",
                            targetPort: 9999)

def mc = deproxy.makeRequest(url: "http://localhost:8080/path/to/resource")

assert mc.receivedResponse.code == "200"
assert mc.handlings.size() == 1

How To: Auxiliary Service

A more complicated case is when the proxy has to call out to some auxiliary service for additional information.

 ________                         ________                        ________
|        |  --->  Request  --->  |        |  ---> Request  --->  |        |
| Client |                       | Proxy  |                      | Server |
|________|  <---  Response <---  |________|  <--- Response <---  |________|

                                   |    ^
                           Request |    |  Response
                                   v    |
                                  ________
                                 |  Aux.  |
                                 |Service |
                                 |________|

An excellent example would be an authentication system. The client sends the request to the proxy and includes credentials. In order to determine if the credentials are valid, the proxy makes a separate HTTP request to the auth service, which then responds with yea or nay. Depending on whether the credentials are valid or not, the proxy will either forward the request on to the server, or return an error back to the client.

In this setup, we can simulate both the server and the auxiliary service with Endpoint objects. The endpoint representing the auth service would have to be given a custom handler, that could interpret and respond to the authentication requests that the proxy makes according to whatever contract is necessary.

Deproxy deproxy = new Deproxy()

def endpoint = deproxy.addEndpoint(9999)

def authResponder = new AuthResponder()
def authService = deproxy.addEndpoint(7777, defaultHandler: authResponder.handler)

def theProxy = new TheProxy(port: 8080,
                            targetHostname: "localhost",
                            targetPort: 9999,
                            authServiceHostname: "localhost",
                            authServicePort: 7777)

def mc = deproxy.makeRequest(url: "http://localhost:8080/path/to/resource",
                             headers: ['X-User': 'valid-user'])

assert mc.receivedResponse.code == "200"
assert mc.handlings.size() == 1
assert mc.handlings[0].endpoint == endpoint
assert mc.orphanedHandlings.size() == 1
assert mc.orphanedHandlings[0].endpoint == authService

def mc = deproxy.makeRequest(url: "http://localhost:8080/path/to/resource",
                             headers: ['X-User': 'invalid-user'])

assert mc.receivedResponse.code == "403"
assert mc.handlings.size() == 0
assert mc.orphanedHandlings.size() == 1
assert mc.orphanedHandlings[0].endpoint == authService

How To: Multiple Servers

Sometimes, a proxy might be set up in front of multiple servers.

                                                                  ________
                                   .------------> Request  --->  |        |
                                   |                             | Server1|
                                   |    .-------- Response <---  |________|
                                   |    |
                                   |    v
 ________                         ________                        ________
|        |  --->  Request  --->  |        |  ---> Request  --->  |        |
| Client |                       | Proxy  |                      | Server2|
|________|  <---  Response <---  |________|  <--- Response <---  |________|

                                   ^    |
                                   |    |                         ________
                                   |    `-------- Request  --->  |        |
                                   |                             | Server3|
                                   `------------- Response <---  |________|

This might be the case, for example, if it is acting as a load balancer, or providing access to different versions of a ReST api based on uri. This is simple enough to simulate by creating multiple endpoint objects, and configuring the proxy to forward client requests to them.

Deproxy deproxy = new Deproxy()

def endpoint1 = deproxy.addEndpoint(9999)
def endpoint2 = deproxy.addEndpoint(9998)
def endpoint3 = deproxy.addEndpoint(9997)

def theProxy = new TheProxy(port: 8080,
                            targets: [
                                ['hostname': "localhost", port: 9999],
                                ['hostname': "localhost", port: 9998],
                                ['hostname': "localhost", port: 9997]],
                            loadBalanceBehavior: Behavior.RoundRobin)

def mc = deproxy.makeRequest(url: "http://localhost:8080/path/to/resource")

assert mc.receivedResponse.code == "200"
assert mc.handlings.size() == 1
assert mc.handlings[0].endpoint == endpoint1

def mc = deproxy.makeRequest(url: "http://localhost:8080/path/to/resource")

assert mc.receivedResponse.code == "200"
assert mc.handlings.size() == 1
assert mc.handlings[0].endpoint == endpoint2

def mc = deproxy.makeRequest(url: "http://localhost:8080/path/to/resource")

assert mc.receivedResponse.code == "200"
assert mc.handlings.size() == 1
assert mc.handlings[0].endpoint == endpoint3

Routing

Even more complex situations can be created using the Route built-in handler, to route requests to existing servers.

 ________          ________          ________        ________
|        |  --->  |        |  --->  |  Fake  | ---> |  Real  |
| Client |        | Proxy  |        | Server |      | Server |
|________|  <---  |________|  <---  |________| <--- |________|

                    |    ^
                    v    |
                   ________
                  |Fake Aux|
                  |Service |
                  |________|

                    |    ^
                    v    |
                   ________
                  |Real Aux|
                  |Service |
                  |________|

This will work for requests from the proxy to an auxiliary service, or from the client/proxy to the server. It can save us the trouble of implementing our own handlers to simulate the server or auxiliary service. But why this round-about way of doing it? Why not just configure the proxy to send requests to those locations directly? The real advantage to this method is that the requests go through an endpoint, so the Request and Response get captured and attached to a MessageChain. When makeRequest returns, we can make assertions against those requests and responses, which would be entirely invisible to us if the proxy had sent them directly.

Deproxy deproxy = new Deproxy()

def endpoint = deproxy.addEndpoint(9999,
        defaultHandler: Handlers.Route("real.server.example.com"))

def authService = deproxy.addEndpoint(7777,
        defaultHandler: Handlers.Route("real.auth.service.example.com"))

def theProxy = new TheProxy(port: 8080,
                            targetHostname: "localhost",
                            targetPort: 9999,
                            authServiceHostname: "localhost",
                            authServicePort: 7777)

Default Response Headers

By default, an endpoint will add a number of headers on all out-bound responses. This behavior can be turned off in custom handlers by setting the HandlerContext’s sendDefaultResponseHeaders field to false (it is true by default). This can be useful for testing how a proxy responds to a misbehaving origin server. Each of the following headers is added if it has not already been explicitly added by the handler, and subject to certain conditions (e.g., presence of a response body):

  • Server
    The identifying information of the server software, “deproxy” followed by the version number.
  • Date
    The date and time at which the response was returned by the handler, in RFC 1123 format.
  • Content-Type
    If the response contains a body, then the endpoint will try to guess. If the body is of type String, then it will add a Content-Type header with a value of text/plain, If the body is of type byte[], it will use a value of application/octet-stream. If the response does not contain a body, then this header will not be added.
  • Transfer-Encoding
    If the response has a body, and the usedChunkedTransferEncoding field is true, this header will have a value of chunked. If it has a body but usedChunkedTransferEncoding is false, the header will have a value of identity. If there is no body, then this header will not be added.
  • Content-Length
    If the response has a body, and the usedChunkedTransferEncoding field is false, then this header will have a value equal to the decimal count of octets in the body. If the body is a String, then the length is the number of bytes after encoding as ASCII. If the body is of type byte[], then the length is just the number of bytes in the array. If the response has a body, but usedChunkedTransferEncoding is true, then this field is not added. If the response does not have a body, then this header will be added with a value of 0.
  • Deproxy-Request-ID
    If the response is associated with a message chain, then the ID of that message chain is assigned to this header and added to the response.

Note: If the response has a body, and sendDefaultResponseHeaders is set to false, and the handler doesn’t explicitly set the Transfer-Encoding header or the Content-Length header, then the client/proxy may not be able to correctly read the response body.