Skip to content

Latest commit

 

History

History
322 lines (267 loc) · 7.8 KB

README.md

File metadata and controls

322 lines (267 loc) · 7.8 KB

Request Sequences

Imagine, there are two micro-services, and we want to pass to the second service data from the response of the first. With Couper, we can connect both services, without one service knowing the other, by using an implicit sequence.

Let's look at an example:

Imagine a math service with two endpoints: /add calculates the sum of two numbers given as a JSON array, /multiply also accepts an array of two numbers and multiplies the first by the second.

In couper-math.hcl, we set up a server connecting the two endpoints:

server {
  hosts = ["*:8080"]
  api {
    endpoint "/connect" {
      # proxy: pass the client request body to /add
      proxy "add" {
        url = "http://math:8081/add"
        # store response in backend_responses.add
      }
      # "default" request: pass response to client
      request {
        url = "http://math:8081/multiply"
        json_body = [ backend_responses.add.json_body.result, 4 ]
      }
    }
  }
}

The proxy configured by the proxy block labelled "add" sends the client request to the first service endpoint (/add) and stores the result in backend_responses.add. The request configured by the request block without a label (so having the implicit label "default") sends a new array with two numbers: the result of the first computation, and 4. As the request block has no label, the response from this request is then passed to the client.

By using a reference to proxy "add" in an attribute of the request block, Couper knows that it has to send the default request only after having received the response from the proxy. Without such references proxy requests and an explicit requests are sent in parallel.

Let's start the example:

$ docker-compose -f docker-compose-math.yml up

And try it by sending the numbers 12 and 34:

$ curl -si -H "Content-Type: application/json" -d '[12, 34]' localhost:8080/connect
HTTP/1.1 200 OK
Content-Type: application/json
...

{"result":184}

The /add service calculated the sum of 12 and 34 (= 46), the /multiply calculated the product of the result of /add (46) and 4 (= 184).

Fine!

What happens if we don't pass an array with two numbers, but [12, "foo"]?

$ curl -si -H "Content-Type: application/json" -d '[12, "foo"]' localhost:8080/connect
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
...

{
  "error": {
    "id":      "c71kp4t916bht5pkuqag",
    "message": "expression evaluation error",
    "path":    "/multiply",
    "status":  500
  }
}

Hmm, this is an error from the /multiply endpoint. But looking at the logs, we see that already the request to the /add endpoint returned a 500:

{
  "backend":"default",
  "request": {
    "name": "add"
  },
  "status":500,
  "type":"couper_backend",
  "url":"http://math:8081/add"
}

Additionally, we see another entry in the couper_backend log:

{
  "backend":"default",
  "request": {
    "name": "default"
  },
  "status":500,
  "type":"couper_backend"
  "url":"http://math:8081/multiply"
}

So Couper sent the second request, even though the first produced an unexpected result.

But we can stop the sequence earlier by configuring the expected status code for each request:

# ...
      proxy "add" {
        ...
        expected_status = [200]                  #
      }
      # "default" request: pass response to client
      request {
        ...
        expected_status = [200]                  #
      }
# ...
$ curl -si -H "Content-Type: application/json" -d '[12, "foo"]' localhost:8080/connect
HTTP/1.1 502 Bad Gateway
Content-Type: application/json
Couper-Error: endpoint error
...

{
  "error": {
    "id":      "c71kuad916bht5pkuqb0",
    "message": "endpoint error",
    "path":    "/connect",
    "status":  502
  }
}

Now we see only one entry in the couper_backend log (for /add).

We also see an error in the couper_access log:

{
  "error_type": "unexpected_status",
  "level": "error",
  "message": "endpoint error",
  "type": "couper_access"
}

Let's add an error_handler to handle the error:

...
      request {
        ...
        expected_status = [200]
      }
      error_handler "unexpected_status" {
        response {
          status = 500
          json_body = {
            error = "upstream error"
            error_description = "an upstream service responded with an unexpected status code"
          }
        }
      }
# ...
$ curl -si -H "Content-Type: application/json" -d '[12, "foo"]' localhost:8080/connect
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
...

{"error":"upstream error","error_description":"an upstream service responded with an unexpected status code"}

And we can log some additionaly information about the requests in the case of an error:

# ...
      error_handler "unexpected_status" {
        response {
          ...
        }
        custom_log_fields = {
          add = {
            message = backend_responses.add.json_body.error.message
            status = backend_responses.add.json_body.error.status
          }
          default = {
            message = backend_responses.default.json_body.error.message
            status = backend_responses.default.json_body.error.status
          }
        }
      }
# ...

This adds a new field to the log message:

{
  "custom": {
    "add": {
      "message": "expression evaluation error",
      "status": 500
    }
  },
  "error_type": "unexpected_status"
}

showing that the proxy "add" responded with a status code 500 and the error message "expression evaluation error".

If we change the request to

# ...
      request {
        url = "http://math:8081/multiply"
        json_body = [ backend_responses.add.json_body.result, "bar" ]  # ← "bar" instead of 4
      }
# ...

and send a "proper" array, the custom field in the log message now shows that proxy "add" produced a "proper" result, while request "default" has an error:

{
  "custom": {
    "add": {},
    "default": {
      "message": "expression evaluation error",
      "status": 500
    }
  },
  "error_type": "unexpected_status"
}

Another use case for a sequence is the provisioning of an access token prior to a request that must be authorized.

See couper_auth.hcl for an example:

server {
  hosts = ["*:8080"]
  api {
    endpoint "/" {
      request "token" {
        url = "http://token-provider:8081/token"
        form_body = {
          sub = "myself"
        }
        expected_status = [200]
      }
      # The reference to backend_responses.token makes Couper wait for request "token"'s response.
      request "pr" {
        url = "http://resource:8082/protected-res"
        headers = {
          authorization = "Bearer ${backend_responses.token.json_body.access_token}"
        }
        json_body = { a = true, b = 2 }
      }
      response {
        json_body = {
          pr = backend_responses.pr.json_body
        }
      }
    }
  }
}

Couper creates a sequence consisting of both requests.

Let's start the example:

$ docker-compose -f docker-compose-auth.yml up

and call the endpoint:

$ curl -si localhost:8080/
HTTP/1.1 200 OK
Content-Type: application/json

{"pr":{"you_sent":{"a":true,"b":2}}}

In the backend log we see two entries:

{
  "request":{
    "name":"token"
  },
  "status":200,
  "type":"couper_backend",
  "url":"http://token-provider:8081/token"
}
{
  "depends_on":"token",
  "request":{
    "name":"pr"
  },
  "status":200,
  "type":"couper_backend",
  "url":"http://resource:8082/protected-res"
}

The request named pr depends_on the response for the request named token.

If we added another request without any reference to one of the requests in the sequence, this request would be started in parallel with the sequence.