Skip to content

feat: add FFI functions to populate upstream state variables#112

Merged
nic-6443 merged 8 commits into
mainfrom
nic/upstream-state
Apr 28, 2026
Merged

feat: add FFI functions to populate upstream state variables#112
nic-6443 merged 8 commits into
mainfrom
nic/upstream-state

Conversation

@jarvis9443
Copy link
Copy Markdown
Contributor

@jarvis9443 jarvis9443 commented Apr 27, 2026

What

Add C FFI functions (push_upstream_state / update_upstream_state) that allow Lua code to populate r->upstream_states, making nginx upstream variables ($upstream_status, $upstream_addr, $upstream_response_time, $upstream_header_time, $upstream_connect_time, $upstream_response_length) available in access logs when the upstream module is bypassed (e.g., when using cosocket transport).

Why

When plugins like ai-proxy use cosocket (resty.http) instead of proxy_pass, nginx upstream variables are empty in access logs because r->upstream_states is never populated.

API

2-step design:

  1. push_upstream_state(opts) — called when headers arrive or on error

    • Sets: addr, status, connect_time, header_time
    • Supports multiple calls for retry scenarios ("502, 200" semantics)
  2. update_upstream_state(opts) — called after body is fully consumed

    • Sets: response_time, response_length

Changes

  • src/ngx_http_apisix_module.c/.h: C FFI functions
  • lib/resty/apisix/upstream.lua: Lua FFI wrappers with input validation
  • t/upstream_state.t: 8 test cases (unit via ngx.var + integration via access_log)
  • .github/workflows/ci.yml: Switch from legacy build-apisix-base.sh to production runtime build scripts (build-apisix-runtime.sh for OR 1.27, build-api7ee-runtime.sh for OR 1.21)

Add push_upstream_state() and update_upstream_state() C FFI functions that
populate r->upstream_states, enabling standard nginx upstream variables
($upstream_status, $upstream_addr, $upstream_response_time, etc.) to be
set from Lua when the upstream module is bypassed (e.g., ai-proxy cosocket).

2-step API:
- push: creates a new upstream state entry with addr/status/connect_time/header_time
- update: fills in final response_time/response_length on the last entry

Unset timing fields default to -1 (renders as "-" in logs, not "0.000").
Each retry attempt can push a separate entry, matching nginx multi-value
semantics (e.g., "502, 200" in $upstream_status).
Copilot AI review requested due to automatic review settings April 27, 2026 06:56
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds two Lua APIs—push_upstream_state(opts) and update_upstream_state(opts)—and corresponding C functions that push and update per-request upstream state. New TAP tests validate variable exposure, sentinel defaults, multi-record sequences, and error behavior when updating without a prior push.

Changes

Cohort / File(s) Summary
Lua API
lib/resty/apisix/upstream.lua
Adds _M.push_upstream_state(opts) and _M.update_upstream_state(opts). Both require opts be a table and a non-nil request from get_request(). push_upstream_state validates opts.addr (if present), derives addr_len (0 when absent), applies defaults (status→0, connect_time_ms/header_time_ms→-1), calls C.ngx_http_apisix_push_upstream_state, and returns true or nil, err when FFI returns ngx.ERROR. update_upstream_state passes response_time/response_length (defaults -1) to C.ngx_http_apisix_update_upstream_state and returns true or nil, err.
C implementation
src/ngx_http_apisix_module.c
Adds ngx_http_apisix_push_upstream_state(...) to ensure/allocate r->upstream_states, push a zeroed upstream state, optionally copy peer addr into state->peer, set status, and set connect_time/header_time only when inputs are non-negative (otherwise leave as sentinel -1). Adds ngx_http_apisix_update_upstream_state(...) to target the most-recent pushed state and update response_time and/or response_length only when provided values are non-negative; both return NGX_OK on success and NGX_ERROR on missing state or allocation failure.
C header
src/ngx_http_apisix_module.h
Declares the two new upstream-state functions added to the C implementation.
Tests
t/upstream_state.t
New TAP tests covering push/update flows: verifies ngx.var.upstream_status, ngx.var.upstream_addr, timing formatting and sentinel/default values, multi-push/update producing comma-separated vars, error when updating before pushing, and visibility of updated response time/length via nginx vars.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Lua as lib/resty/apisix/upstream.lua
  participant FFI_C as C (ngx_http_apisix_module)
  participant Request as ngx_http_request_t
  participant NginxVars as nginx vars / readers

  Client->>Lua: call push_upstream_state(opts)
  Lua->>Request: get_request()
  Lua->>FFI_C: C.ngx_http_apisix_push_upstream_state(r, addr?, addr_len, status, connect_ms, header_ms)
  FFI_C->>Request: ensure r->upstream_states, push new upstream state, copy addr/status, set timings if >= 0
  Request->>NginxVars: expose upstream_status / upstream_addr / timing fields
  Client->>Lua: call update_upstream_state(opts)
  Lua->>FFI_C: C.ngx_http_apisix_update_upstream_state(r, response_ms, response_len)
  FFI_C->>Request: update last upstream state response_time/response_length when >= 0
  Request->>NginxVars: upstream_response_time / upstream_response_length updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Security Check ❓ Inconclusive Unable to verify custom requirements - function calls attempted but produced no output indicating actual file content or verification details Provide actual file contents and specific verification requirements to assess compliance
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
E2e Test Quality Review ✅ Passed PR demonstrates solid E2E test coverage with seven TAP integration scenarios covering the full flow from Lua API through FFI to C implementation and nginx variables, with robust error handling at all layers and scope-appropriate implementation.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add FFI functions to populate upstream state variables' accurately summarizes the main change—adding FFI functions to populate upstream state. It is clear, specific, and directly reflects the changeset's primary objective.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nic/upstream-state

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds C FFI + Lua wrapper APIs to manually populate r->upstream_states for cosocket-based upstream requests, so standard nginx $upstream_* variables become available for access logs and Lua-variable reads even when the upstream module is bypassed.

Changes:

  • Add ngx_http_apisix_push_upstream_state() / ngx_http_apisix_update_upstream_state() to create/update entries in r->upstream_states.
  • Expose Lua APIs push_upstream_state() / update_upstream_state() via lib/resty/apisix/upstream.lua.
  • Add t/upstream_state.t coverage for status/addr, timings, dash rendering, retry semantics, and error path.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
t/upstream_state.t Adds integration tests validating $upstream_* variable population and formatting.
src/ngx_http_apisix_module.h Declares new C functions for pushing/updating upstream state.
src/ngx_http_apisix_module.c Implements upstream state creation/update logic against r->upstream_states.
lib/resty/apisix/upstream.lua Adds LuaJIT FFI bindings and Lua-facing helper functions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/resty/apisix/upstream.lua
Comment thread lib/resty/apisix/upstream.lua
Comment thread lib/resty/apisix/upstream.lua
log_format directive is only valid in http context, not location.
Rewrite tests to verify upstream variables via ngx.var reads.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
t/upstream_state.t (1)

123-142: Optional: assert return values in retry test for better failure diagnostics.

This block currently ignores ok, err; checking them would make failures easier to pinpoint when regressions occur.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@t/upstream_state.t` around lines 123 - 142, The test currently ignores the
return values from upstream.push_upstream_state and
upstream.update_upstream_state; update the retry test to capture and assert the
returned ok, err (or equivalent) for each call to upstream.push_upstream_state
and upstream.update_upstream_state so failures show precise diagnostics—e.g.,
assign results from upstream.push_upstream_state({ ... }) and assert ok is
truthy (and err is nil or contains expected text), and do the same for each
upstream.update_upstream_state call for both the failed and succeeding attempts.
lib/resty/apisix/upstream.lua (1)

134-171: Add defensive opts validation to avoid hard runtime exceptions.

Line 140 and Line 164 index opts directly; passing nil/non-table will throw instead of returning a controlled API error.

Proposed hardening
 function _M.push_upstream_state(opts)
+    if type(opts) ~= "table" then
+        return nil, "opts expects a table"
+    end
+
     local r = get_request()
     if not r then
         return nil, "no request found"
@@
 function _M.update_upstream_state(opts)
+    if type(opts) ~= "table" then
+        return nil, "opts expects a table"
+    end
+
     local r = get_request()
     if not r then
         return nil, "no request found"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/resty/apisix/upstream.lua` around lines 134 - 171, Both
_M.push_upstream_state and _M.update_upstream_state assume opts is a table and
index into it directly; add a defensive check at the start of each function to
validate opts is a table (or at least non-nil) and return nil plus a clear error
string when it's invalid, so accessing opts.addr, opts.status,
opts.connect_time, opts.header_time, opts.response_time and opts.response_length
cannot raise a runtime error; update the functions to perform that check before
calling get_request() or before using any opts fields and keep existing return
values/NGX_ERROR handling unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/resty/apisix/upstream.lua`:
- Around line 16-27: The FFI cdef currently hardcodes "typedef long off_t" and
uses ngx_int_t for millisecond time params, which mismatches the C API; update
the cdef so off_t is defined in a platform-aware way (or removed to match the
system typedef) instead of forcing "long", and change the time parameter types
from ngx_int_t to ngx_msec_int_t for the functions
ngx_http_apisix_push_upstream_state (connect_time_ms, header_time_ms) and
ngx_http_apisix_update_upstream_state (response_time_ms); keep all other
signatures unchanged so the Lua FFI aligns exactly with the C header types.

---

Nitpick comments:
In `@lib/resty/apisix/upstream.lua`:
- Around line 134-171: Both _M.push_upstream_state and _M.update_upstream_state
assume opts is a table and index into it directly; add a defensive check at the
start of each function to validate opts is a table (or at least non-nil) and
return nil plus a clear error string when it's invalid, so accessing opts.addr,
opts.status, opts.connect_time, opts.header_time, opts.response_time and
opts.response_length cannot raise a runtime error; update the functions to
perform that check before calling get_request() or before using any opts fields
and keep existing return values/NGX_ERROR handling unchanged.

In `@t/upstream_state.t`:
- Around line 123-142: The test currently ignores the return values from
upstream.push_upstream_state and upstream.update_upstream_state; update the
retry test to capture and assert the returned ok, err (or equivalent) for each
call to upstream.push_upstream_state and upstream.update_upstream_state so
failures show precise diagnostics—e.g., assign results from
upstream.push_upstream_state({ ... }) and assert ok is truthy (and err is nil or
contains expected text), and do the same for each upstream.update_upstream_state
call for both the failed and succeeding attempts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 06839f44-2568-4a9f-bb5d-a725846b4607

📥 Commits

Reviewing files that changed from the base of the PR and between 0327bd7 and cb04e64.

📒 Files selected for processing (4)
  • lib/resty/apisix/upstream.lua
  • src/ngx_http_apisix_module.c
  • src/ngx_http_apisix_module.h
  • t/upstream_state.t

Comment thread lib/resty/apisix/upstream.lua Outdated
- Remove duplicate off_t typedef (conflicts with client.lua), use intptr_t
- Add input validation for push/update_upstream_state opts parameter
Copilot AI review requested due to automatic review settings April 27, 2026 07:05
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/resty/apisix/upstream.lua
Comment thread lib/resty/apisix/upstream.lua
Comment thread t/upstream_state.t Outdated
- response_length is off_t, not ngx_msec_t; -1 renders as '-1' not '-'
  Match nginx behavior: leave at 0 from memzero
- Remove resty.ngxvar dependency from TEST 7 (not in CI env)
- Fix TEST 3 expected: response_length shows '0' when unset
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/resty/apisix/upstream.lua`:
- Around line 143-150: Validate and sanitize option field types before calling
the FFI functions to avoid LuaJIT FFI argument errors: ensure opts.addr is
either nil or a string (get its length only after confirming string), and ensure
numeric fields (opts.status, opts.connect_time, opts.header_time, and the other
numeric field used in the later FFI call at the 169-172 block) are numbers
(coerce with tonumber or return nil, err if invalid) before invoking
C.ngx_http_apisix_push_upstream_state (and the similar FFI call in the 169-172
block); return nil, err on invalid types instead of passing bad values into the
FFI call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e424d64e-daa3-49bc-a992-366cfce40abc

📥 Commits

Reviewing files that changed from the base of the PR and between 42eb5b0 and 5ab0e69.

📒 Files selected for processing (1)
  • lib/resty/apisix/upstream.lua

Comment thread lib/resty/apisix/upstream.lua
Copilot AI review requested due to automatic review settings April 27, 2026 07:20
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
t/upstream_state.t (1)

122-157: Capture and assert ok, err for push/update in these tests.

These blocks call push_upstream_state/update_upstream_state without checking return values. Adding explicit checks makes failures more actionable and avoids partial silent behavior.

Also applies to: 180-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@t/upstream_state.t` around lines 122 - 157, The tests call
push_upstream_state and update_upstream_state without checking their return
values; modify each invocation to capture the returned ok, err (e.g., local ok,
err = upstream.push_upstream_state(...)) and immediately assert success
(assert(ok, err) or the test framework's assert) so failures surface with the
error message; apply the same change for all occurrences (including the other
block around lines 180-210) referencing push_upstream_state and
update_upstream_state to ensure each call's result is validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@t/upstream_state.t`:
- Around line 122-157: The tests call push_upstream_state and
update_upstream_state without checking their return values; modify each
invocation to capture the returned ok, err (e.g., local ok, err =
upstream.push_upstream_state(...)) and immediately assert success (assert(ok,
err) or the test framework's assert) so failures surface with the error message;
apply the same change for all occurrences (including the other block around
lines 180-210) referencing push_upstream_state and update_upstream_state to
ensure each call's result is validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f56693e-2285-4e69-b9f7-cc96da6a5745

📥 Commits

Reviewing files that changed from the base of the PR and between 5ab0e69 and 033f569.

📒 Files selected for processing (2)
  • src/ngx_http_apisix_module.c
  • t/upstream_state.t

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/resty/apisix/upstream.lua
Comment thread lib/resty/apisix/upstream.lua
Comment thread t/upstream_state.t
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
lib/resty/apisix/upstream.lua (1)

148-153: ⚠️ Potential issue | 🟠 Major

Validate numeric option fields before FFI invocation.

Line 151–153 and Line 174–175 pass user-provided values directly to FFI. Non-numeric inputs can raise LuaJIT conversion exceptions and bypass the intended nil, err contract.

In LuaJIT FFI, what happens when a C function parameter is `int`/`intptr_t` and the caller passes a Lua string or table value?
💡 Proposed fix
+local function validate_number_field(v, name)
+    if v ~= nil and type(v) ~= "number" then
+        return nil, name .. " must be a number"
+    end
+    return true
+end
+
 function _M.push_upstream_state(opts)
@@
     local addr = opts.addr
     if addr ~= nil and type(addr) ~= "string" then
         return nil, "addr must be a string"
     end
+    local ok, err = validate_number_field(opts.status, "status")
+    if not ok then
+        return nil, err
+    end
+    ok, err = validate_number_field(opts.connect_time, "connect_time")
+    if not ok then
+        return nil, err
+    end
+    ok, err = validate_number_field(opts.header_time, "header_time")
+    if not ok then
+        return nil, err
+    end
+
     local addr_len = addr and `#addr` or 0
@@
 function _M.update_upstream_state(opts)
@@
+    local ok, err = validate_number_field(opts.response_time, "response_time")
+    if not ok then
+        return nil, err
+    end
+    ok, err = validate_number_field(opts.response_length, "response_length")
+    if not ok then
+        return nil, err
+    end
+
     local ret = C.ngx_http_apisix_update_upstream_state(
         r,
         opts.response_time or -1,

Also applies to: 172-175

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/resty/apisix/upstream.lua` around lines 148 - 153, Validate and sanitize
numeric fields before calling the FFI functions: for the
C.ngx_http_apisix_push_upstream_state invocation(s) (and the other similar call
at the later lines), coerce opts.status, opts.connect_time, opts.header_time
(and any other numeric opts passed into C.*) with tonumber and explicitly handle
non-numeric values by returning nil, "invalid numeric field: <field_name>" (or a
similar error) instead of passing them directly to FFI; if tonumber returns nil
use the documented default (-1 or 0 as appropriate) only when the option is
absent, otherwise treat an explicit non-numeric value as an error so LuaJIT
conversion exceptions are avoided and the nil, err contract is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/resty/apisix/upstream.lua`:
- Around line 148-153: Validate and sanitize numeric fields before calling the
FFI functions: for the C.ngx_http_apisix_push_upstream_state invocation(s) (and
the other similar call at the later lines), coerce opts.status,
opts.connect_time, opts.header_time (and any other numeric opts passed into C.*)
with tonumber and explicitly handle non-numeric values by returning nil,
"invalid numeric field: <field_name>" (or a similar error) instead of passing
them directly to FFI; if tonumber returns nil use the documented default (-1 or
0 as appropriate) only when the option is absent, otherwise treat an explicit
non-numeric value as an error so LuaJIT conversion exceptions are avoided and
the nil, err contract is preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3980bdc9-2c06-40ce-89cd-4e910dca0187

📥 Commits

Reviewing files that changed from the base of the PR and between 033f569 and dd09170.

📒 Files selected for processing (1)
  • lib/resty/apisix/upstream.lua

- Add TEST 8: cosocket HTTP request to mock upstream + access_log
  verification via regex (mirrors ai-proxy's real usage pattern)
- Switch CI from legacy build-apisix-base.sh to production runtime
  build scripts: build-apisix-runtime.sh (OR 1.27) and
  build-api7ee-runtime.sh (OR 1.21)
- Replace OR version matrix with runtime type matrix matching the
  two production runtimes (apisix-runtime and api7ee-runtime)
@jarvis9443 jarvis9443 changed the title feat: add FFI functions to populate upstream state for cosocket requests feat: add FFI functions to populate upstream state variables Apr 28, 2026
Copilot AI review requested due to automatic review settings April 28, 2026 01:02
Comment thread .github/workflows/ci.yml
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/ci.yml
t/pipe.t TEST 8 and t/upstream_pass_trailers.t rely on nginx features
not available in OpenResty 1.21.4.4 (nginx 1.21). These are pre-existing
incompatibilities, not caused by this PR.
@nic-6443 nic-6443 requested a review from membphis April 28, 2026 01:57
@nic-6443 nic-6443 merged commit 555c644 into main Apr 28, 2026
5 checks passed
@nic-6443 nic-6443 deleted the nic/upstream-state branch April 28, 2026 08:40
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.

4 participants