/ APISIX, ABSTRACTION

Apache APISIX plugin priority, a leaky abstraction?

Apache APISIX is an API Gateway, which builds upon the OpenResty reverse-proxy to offer a plugin-based architecture. The main benefit of such an architecture is that it brings structure to the configuration of routes. It’s a help at scale, when managing hundreds or thousands of routes.

In this post, I’d like to describe how plugins, priority, and phases play together and what pitfalls you must be aware of.

APISIX plugin’s priority

When you configure a route with multiple plugins, Apache APISIX needs to execute them in a consistent order so that the results are the same over time. For this reason, every APISIX plugin has a harcoded priority. You can check a plugin priority directly in the code. For example, here’s the relevant code fragment for the basic-auth plugin:

local _M = {
    version = 0.1,
    priority = 2520,                                 (1)
    type = 'auth',
    name = plugin_name,
    schema = schema,
    consumer_schema = consumer_schema
}
1 Priority is 2520

For documentation purposes, the default-config.yaml file lists the priority of all out-of-the-box plugins. Here’s an excerpt:

plugins:
  - ldap-auth                      # priority: 2540
  - hmac-auth                      # priority: 2530
  - basic-auth                     # priority: 2520
  - jwt-auth                       # priority: 2510
  - key-auth                       # priority: 2500
  - consumer-restriction           # priority: 2400
  - forward-auth                   # priority: 2002
  - opa                            # priority: 2001
  - authz-keycloak                 # priority: 2000
  #- error-log-logger              # priority: 1091
  - proxy-cache                    # priority: 1085
  - body-transformer               # priority: 1080
  - proxy-mirror                   # priority: 1010
  - proxy-rewrite                  # priority: 1008
  - workflow                        # priority: 1006
  - api-breaker                    # priority: 1005
  - limit-conn                     # priority: 1003
  - limit-count                    # priority: 1002
  - limit-req                      # priority: 1001

Imagine a route configured with proxy-mirror and proxy-rewrite. Because of their respective priority, proxy-mirror would run before proxy-rewrite.

upstream:
  - id: 1
    nodes:
      "oldapi:8081": 1

routes:
  - id: 1
    uri: "/v1/hello*"
    upstream_id: 1
    plugins:
      proxy-rewrite:                                 (1) (3)
        regex_uri: ["/v1(.*)", "$1"]
      proxy-mirror:                                  (2)
        host: http://new.api:8082
1 The plugin ordering in this file is not relevant
2 proxy-mirror has priority 1010 and will execute first
3 proxy-rewrite has priority 1008 and will run second

The above setup has an issue. For example, if we call localhost:9080/v1/hello, APISIX will first mirror the request, then remove the /v1 prefix. Hence, the new API receives /v1/hello instead of /hello. It’s possible to override the default priority to fix it:

routes:
  - id: 1
    uri: "/v1/hello*"
    upstream_id: 1
    plugins:
      proxy-rewrite:
        _meta:
          priority: 1020                             (1)
        regex_uri: ["/v1(.*)", "$1"]
      proxy-mirror:
        host: http://new.api:8082
1 Override the default priority

Now, proxy-rewrite has higher priority than proxy-mirror: the former runs before the latter.

In this case, it works flawlessly; in others, it might not. Let’s dive further.

APISIX phases

APISIX runs plugins not only according to their priorities but also through dedicated phases. Because APISIX builds upon OpenResty, which builds upon ngxinx, the phases are very similar to the phases of these two components.

nginx defines several phases an HTTP request goes through:

  1. NGX_HTTP_POST_READ_PHASE
  2. NGX_HTTP_SERVER_REWRITE_PHASE
  3. NGX_HTTP_FIND_CONFIG_PHASE
  4. NGX_HTTP_REWRITE_PHASE
  5. NGX_HTTP_POST_REWRITE_PHASE
  6. NGX_HTTP_PREACCESS_PHASE
  7. NGX_HTTP_ACCESS_PHASE
  8. NGX_HTTP_POST_ACCESS_PHASE
  9. NGX_HTTP_PRECONTENT_PHASE
  10. NGX_HTTP_CONTENT_PHASE
  11. NGX_HTTP_LOG_PHASE

Each phase focuses on a task, i.e., NGX_HTTP_ACCESS_PHASE verifies that the client is authorized to make the request.

In turn, OpenResty offers similarly named phases.

Finally, here are the phases of Apache APISIX:

  1. rewrite
  2. access
  3. before_proxy
  4. header_filter
  5. body_filter
  6. log

We have seen two ways to order plugins: by priority and by phase. Now comes the most important rule: order by priority only works inside a phase!

For example, the redirect plugin has a priority of 900 and runs in phase rewrite; the gzip plugin has a priority of 995 and runs in phase body_filter. Regardless of their respective priorities, redirect will happen before gzip, because rewrite happens before body_filter. Likewise, changing a priority won’t "move" a plugin out of its phase.

The example above with proxy-mirror and proxy-rewrite worked because both run in the rewrite phase.

The main issue is that priority is documented in the config-default.yaml file, while the phase is buried in the code. Worse, some plugins run across different phases. For example, let’s check the proxy proxy-rewrite plugin and, more precisely, the functions defined there:

  • local function is_new_headers_conf(headers)
  • local function check_set_headers(headers)
  • function _M.check_schema(conf)
  • function _M.rewrite(conf, ctx)

The name of the rewrite() function is suspiciously similar to one of the phases above. Looking at other plugins, we see the same pattern repeat. Apache APISIX runs plugin functions with the same name as the phase.

I took the liberty of summarizing all plugins and their respective phases in a table.

Plugin Phase

General

rewrite

access

before_proxy

header_filter

body_filter

log

redirect

echo

gzip

real-ip

ext-plugin-pre-req

ext-plugin-post-req

ext-plugin-post-resp

workflow

Transformation

rewrite

access

before_proxy

header_filter

body_filter

log

response-rewrite

proxy-rewrite

grpc-transcode

grpc-web

fault-injection

mocking

degraphql

body-transformer

Authentication

rewrite

access

before_proxy

header_filter

body_filter

log

key-auth

jwt-auth

basic-auth

authz-keycloak

authz-casdoor

wolf-rbac

openid-connect

cas-auth

hmac-auth

authz-casbin

ldap-auth

opa

forward-auth

Security

rewrite

access

before_proxy

header_filter

body_filter

log

cors

uri-blocker

ua-restriction

referer-restriction

consumer-restriction

csrf

public-api

chaitin-waf

Traffic

rewrite

access

before_proxy

header_filter

body_filter

log

limit-req

limit-conn

limit-count

proxy-cache (init)

proxy-cache (disk)

proxy-cache (memory)

request-validation

proxy-mirror

api-breaker

traffic-split

request-id

proxy-control

client-control

Observability

rewrite

access

before_proxy

header_filter

body_filter

log

zipkin

skywalking

opentelemetry

prometheus

node-status

datadog

http-logger

skywalking-logger

tcp-logger

kafka-logger

rocketmq-logger

udp-logger

clickhouse-logger

syslog

log-rotate

error-log-logger

sls-logger

google-cloud-logging

splunk-hec-logging

file-logger

loggly

elasticsearch-logger

tencent-cloud-cls

loki-logger

Others

rewrite

access

before_proxy

header_filter

body_filter

log

serverless

openwhisk

dubbo-proxy

kafka-proxy

Conclusion

I’ve detailed Apache APISIX plugin phases and priorities in this post. I’ve explained their relationship with one another. Icing on the cake, I documented each out-of-the-box plugin’s phase(s). I hope it will prove helpful.

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Apache APISIX plugin priority, a leaky abstraction?
Share this