apisix icon indicating copy to clipboard operation
apisix copied to clipboard

help request: Dynamic routing based on HTTP_STATUS from first API

Open chilcano opened this issue 1 year ago • 15 comments

Description

Hi All, I would like to route the request to 3rd API based on the 1st API HTTP_STATUS response. I mean:

  1. Call http://apisix/api1
  2. Get api1 response / http_status
  3. If http_status = 200, then call to http://apisix/api2
  4. If http_status = 400, then call to http://apisix/api3

Please, could anyone share an APISIX route snippet to do that?. Any idea is welcome. Regards.

Environment

  • APISIX version (run apisix version):
  • Operating system (run uname -a):
  • OpenResty / Nginx version (run openresty -V or nginx -V):
  • etcd version, if relevant (run curl http://127.0.0.1:9090/v1/server_info):
  • APISIX Dashboard version, if relevant:
  • Plugin runner version, for issues related to plugin runners:
  • LuaRocks version, for installation issues (run luarocks --version):

chilcano avatar Jul 29 '23 11:07 chilcano

This can be achieved by combining several plugins, such as the following:

uri: /internal-check
plugins:
  response-rewrite:
    headers:
      X-Api1-Status: '$status'
    status_code: 200
  proxy-rewrite: 
    uri: "/api1",
upstream:
  nodes:
    - host: apisix
      port: 80
      weight: 1
  type: roundrobin
status: 1
---
uri: /testTrafficSplit
host: apisix
plugins:
  forward-auth:
    upstream_headers:
      - X-Api1-Status
    uri: http://127.0.0.1/internal-check
  serverless-pre-function:
    functions:
      - |
        local core = require('apisix.core');
        return function(conf, ctx)
          if ctx.var.http_x_api1_status == '200' then
            ctx.var.upstream_uri = '/api2'
          elseif ctx.var.http_x_api1_status == '400' then
            ctx.var.upstream_uri = '/api3'
          end
        end
    phase: before_proxy
upstream:
  nodes:
    - host: apisix
      port: 80
      weight: 1
  type: roundrobin
status: 1

qwzhou89 avatar Aug 01 '23 07:08 qwzhou89

Thanks @qwzhou89

Quick questions:

  1. What is $status in the first route?
  2. In the 2nd route. Why not using the /api1 response (HTTP_STATUS or json body) instead of using custom header X-Api1-Status?.

I've tried this approach, but I don't know how get the /api1 response (headers, json body) in the 1st proxy-rewrite plugin from 2nd plugin serverless-post-function.

This is my only 1 route.

{
   "uri": "/api1/*",
   "methods":["GET"],
   "plugins": {
      "proxy-rewrite": {
         "host": "random-data-api.com",
         "uri": "/api/v2/banks",
         "scheme": "https"
      },
      "serverless-post-function": {
         "phase": "body_filter",
         "functions": [
            "return function(conf, ctx)
                  -- Print the iban value of json_response
            end"
         ]
      }
   },
   "upstream_id": 1
}'

The Random Banks API (https://random-data-api.com/api/v2/banks) returns this:

{
  "id": 5243,
  "uid": "f38123fc-7c1d-4705-980f-3e82e770b100",
  "account_number": "0638101451",
  "iban": "GB86RWJD49924585669735",
  "bank_name": "UBS CLEARING AND EXECUTION SERVICES LIMITED",
  "routing_number": "290583418",
  "swift_bic": "BOFAGB3SSWI"
}

And the upstream is:

{
   "name": "Random API upstream",
   "type": "roundrobin",
   "scheme": "https",
   "nodes": {
      "random-data-api.com": 1
   }
}

Should I use serverless-pre-function and different phase ? Or should I use a custom plugin? Appreciate your support.

Regards.

chilcano avatar Aug 01 '23 09:08 chilcano

@chilcano,

What is $status in the first route?

$status is an nginx variable: https://nginx.org/en/docs/http/ngx_http_core_module.html#var_status. This means, the HTTP_STATUS will be stored in the X-Api1-Status http header.

shreemaan-abhishek avatar Aug 01 '23 10:08 shreemaan-abhishek

I don't know how get the /api1 response (headers, json body) in the 1st proxy-rewrite plugin from 2nd plugin serverless-post-function.

Why do you want access the response body and headers? 🤔 If you have very complex requirements you might have to write your own plugin.

shreemaan-abhishek avatar Aug 01 '23 10:08 shreemaan-abhishek

Why do you want access the response body and headers? thinking If you have very complex requirements you might have to write your own plugin.

Thanks @shreemaan-abhishek for your support.

I've tried to extend existing example available here Chaining API requests with API Gateway Demo..

I've created 2 plugins (api1-plugin and api2-plugin ) and use both in the same route. The api1-plugin call to 3rd party api1 and the api1 json response is used to make a call to 3rd party api2 through api2-plugin.

In simple words, I'm trying to chain 2 APIs, however the challenge I'm facing is how to retrieve the response of 1st plugin from 2nd plugin.

I've tried ngx.var.* and ngx.ctx.* to share info from 1st to 2nd plugin and that didn't work.

I'm using 2 plugins because each plugin is calling different upstreams with different requests.

Any idea on how get this? Appreciate your support. Regards.

chilcano avatar Aug 01 '23 11:08 chilcano

I've tried ngx.var.* and ngx.ctx.* to share info from 1st to 2nd plugin and that didn't work.

You can refer this

        local body = core.response.hold_body_chunk(ctx)
        if not body then
            return
        end

https://github.com/apache/apisix/blob/f832ea2502ce2696b0e7390a1572ab7000fef344/apisix/plugins/response-rewrite.lua#L186C9-L186C9

        last_resp, err = httpc:request_uri(node.url, params)
        if not last_resp then
            return 500, "request failed: " .. err
        end

https://github.com/Boburmirzo/apisix-plugin-pipeline-request-demo/blob/3b437f915e4c9ee1c0e928211e3747962c8d50e7/custom-plugins/pipeline-request.lua#L100C10-L100C10

qwzhou89 avatar Aug 01 '23 13:08 qwzhou89

Thanks @qwzhou89

I've found a way to share info from a 1st plugin to 2nd plugin. I enabled nginx.http.custom_lua_shared_dict in the apisix configuration (config.yaml). I used this config:

apisix:
...

nginx_config:
  http:
    custom_lua_shared_dict:
      my_shared_dict: 10m    # Specify the size in MB of the shared memory zone (change as needed)

deployment:
...

plugins:
...

The example here Chaining API requests with API Gateway Demo works only if the 1st call is a GET with not query-string and the 2nd call manipulate the response. What if the 1st call is POST and a json request is sent?

That is why I'm trying to implement in 2 plugins but in the same route. However, now I'm stuck and I can not be able to execute the api1_plugin firstly and api2_plugin later. I've followed the recommendations Plugins execution order, changed the priority, the phase and always the plugin that implements function _M.access(conf, ctx) is executed.

Am I missing something?

How I can execute sequentially different plugins configured in the same route? Any recommendation here is appreciated.

Regards.

chilcano avatar Aug 01 '23 16:08 chilcano

APISIX plugins are executed sequentially through the phases in openresty:

image

i.e plugins that run on the rewrite phase will always execute before the plugins in the access phase irrespective of priority. (the priority is followed strictly among the plugins that run under the same given execution phase).

shreemaan-abhishek avatar Aug 02 '23 06:08 shreemaan-abhishek

Thanks @shreemaan-abhishek for sharing this information.

Definitely, I'm missing out something in my code.

Questions:

1. What libraries are available in each phase?.

I changed the plugin phase from _M.access() to _M.header_filter() and now I'm having this error when the plugin is being executed.

..
2023/08/02 11:21:15 [error] 51#51: *4647263 failed to run header_filter_by_lua*: /usr/local/apisix/apisix/patch.lua:372: API disabled in the context of header_filter_by_lua*
stack traceback:
	[C]: in function 'error'
	/usr/local/openresty/lualib/resty/core/socket/tcp.lua:271: in function 'original_tcp'
	/usr/local/apisix/apisix/patch.lua:372: in function 'ngx_socket_tcp'
	/usr/local/apisix//deps/share/lua/5.1/resty/http.lua:133: in function 'new'
	/opt/apisix/plugins/api2-plugin.lua:41: in function 'make_request_to_url'
	/opt/apisix/plugins/api2-plugin.lua:123: in function 'phase_func'
	/usr/local/apisix/apisix/plugin.lua:1134: in function 'common_phase'
	/usr/local/apisix/apisix/init.lua:760: in function 'http_header_filter_phase'
	header_filter_by_lua:2: in main chunk, client: 172.19.0.1, server: _, request: "GET /mychainedapi/hello HTTP/1.1", host: "127.0.0.1:9080"
...

This means there are not libraries available in that phase. If so, what libraries can be used there?

2. If only some libraries are accessible in each phase, how I can chain the execution of 2 plugins where the response of the 1st plugin is available in the 2nd plugin Each plugin (api1-plugin and api2-plugin) call external Endpoint URL

Any idea is welcome. Regards.

chilcano avatar Aug 02 '23 13:08 chilcano

  1. What libraries are available in each phase?

Can't list them all but it's logical thinking: you cannot modify the response headers even before the HTTP request is sent to the upstream, does it makes sense?

  1. If only some libraries are accessible in each phase, how I can chain the execution of 2 plugins where the response of the 1st plugin is available in the 2nd plugin

I don't have an answer right now, let me think of something.

shreemaan-abhishek avatar Aug 03 '23 02:08 shreemaan-abhishek

Hi @chilcano , Please let me know how did you make this plugin work? I am getting below while starting apisix and configuring the same in config.yaml

2023/08/02 02:15:02 [error] 1743857#1743857: init_by_lua error: error loading module 'socket.core' from file '/usr/local/apisix/apisix/plugins/custom/pipeline-request.lua': /usr/local/apisix/apisix/plugins/custom/pipeline-request.lua: invalid ELF header stack traceback: [C]: at 0x7fb18658e130 [C]: in function 'require' /usr/local/apisix//deps/share/lua/5.1/socket.lua:12: in main chunk [C]: in function 'require' /usr/local/apisix/apisix/patch.lua:20: in main chunk [C]: in function 'require' /usr/local/apisix/apisix/init.lua:28: in main chunk [C]: in function 'require' init_by_lua:3: in main chunk

My configuration in config.yaml apisix: extra_lua_path: "/usr/local/apisix/custom/apisix/plugins/pipeline-request.lua"

plugins:

  • pipeline-request

sanjivkamate avatar Aug 03 '23 05:08 sanjivkamate

Hi @sanjivkamate - I think you should create a separate Github issue, once done, I'm happy to help you.

chilcano avatar Aug 03 '23 08:08 chilcano

@chilcano Were you able to achieve your use case? If not, then feel free to reopen this issue.

Revolyssup avatar Sep 26 '23 12:09 Revolyssup

Please, re-open this entry since I didn't achieve my use case.

chilcano avatar Oct 06 '23 11:10 chilcano

I've tried to chain 2 APIs. I used the pipeline_request plugin explained here Chaining API requests with API Gateway Demo without success. My use case is:

apisix-route-1/<params> -> /api-a/<encoded-params> -> if response contains EURO -> /api-b
                                                   -> if response contains GBP  -> /api-c
  1. apisix-route-1 is called and it encode <params> and pass that to /api-a.
  2. The /api-a/<encoded-params> is called.
  3. If /api-a/<encoded-params> response contains EURO, then call /api-b.
  4. If /api-a/<encoded-params> response contains GBP, then call /api-c.
  5. Send final response to apisix-route-1/<params> call.

I wanted to use pipeline_request plugin to implement conditional logic without success. Any idea or shared code is welcome. Regards.

chilcano avatar Oct 06 '23 12:10 chilcano