Skip to content

Commit 1cbdd0e

Browse files
committed
feat: subrequest support
1 parent 86488b0 commit 1cbdd0e

9 files changed

Lines changed: 661 additions & 57 deletions

File tree

.github/workflows/nginx.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ env:
5454
load_module ${{ github.workspace }}/nginx/objs/ngx_http_awssigv4_module.so;
5555
load_module ${{ github.workspace }}/nginx/objs/ngx_http_curl_module.so;
5656
load_module ${{ github.workspace }}/nginx/objs/ngx_http_shared_dict_module.so;
57+
load_module ${{ github.workspace }}/nginx/objs/ngx_http_subrequest_module.so;
5758
load_module ${{ github.workspace }}/nginx/objs/ngx_http_upstream_custom_module.so;
5859
5960
OPENSSL_VERSION: '3.0.16'

examples/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ name = "shared_dict"
5656
path = "shared_dict.rs"
5757
crate-type = ["cdylib"]
5858

59+
[[example]]
60+
name = "subrequest"
61+
path = "subrequest.rs"
62+
crate-type = ["cdylib"]
63+
5964
[features]
6065
default = ["export-modules", "ngx/vendored"]
6166
# Generate `ngx_modules` table with module exports

examples/config

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ if [ $HTTP = YES ]; then
4747
ngx_rust_module
4848
fi
4949

50+
if :; then
51+
ngx_module_name=ngx_http_subrequest_module
52+
ngx_module_libs=
53+
ngx_rust_target_name=subrequest
54+
55+
ngx_rust_module
56+
fi
57+
5058
if :; then
5159
ngx_module_name=ngx_http_upstream_custom_module
5260
ngx_module_libs=

examples/subrequest.rs

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
use core::fmt::Display;
2+
3+
use ngx::core::Status;
4+
use ngx::http::subrequest::{SubRequestBuilder, SubRequestError};
5+
use ngx::http::{
6+
HTTPStatus, HttpModule, HttpModuleLocationConf, HttpPhase, HttpRequestHandler,
7+
IntoHandlerStatus, Merge, MergeConfigError, Request, add_phase_handler,
8+
};
9+
use ngx::{ngx_log_debug_http, ngx_log_error};
10+
11+
use nginx_sys::{
12+
NGX_CONF_TAKE1, NGX_ERROR, NGX_HTTP_LOC_CONF, NGX_HTTP_LOC_CONF_OFFSET, NGX_LOG_ERR,
13+
ngx_command_t, ngx_conf_t, ngx_flag_t, ngx_http_complex_value_t, ngx_http_module_t,
14+
ngx_http_request_t, ngx_http_send_response, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t,
15+
};
16+
17+
const NGX_CONF_UNSET_FLAG: ngx_flag_t = nginx_sys::NGX_CONF_UNSET as _;
18+
19+
struct SampleHandler;
20+
21+
enum SampleHandlerError {
22+
ContextAllocation,
23+
SubRequestCreation(String),
24+
SubRequest(ngx_int_t),
25+
Response(ngx_int_t),
26+
}
27+
28+
impl<E> From<SubRequestError<E>> for SampleHandlerError
29+
where
30+
E: Display,
31+
{
32+
fn from(e: SubRequestError<E>) -> Self {
33+
SampleHandlerError::SubRequestCreation(e.to_string())
34+
}
35+
}
36+
37+
impl Display for SampleHandlerError {
38+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39+
match self {
40+
SampleHandlerError::ContextAllocation => {
41+
write!(f, "context allocation failed")
42+
}
43+
SampleHandlerError::SubRequestCreation(e) => {
44+
write!(f, "subrequest creation failed: {}", e)
45+
}
46+
SampleHandlerError::SubRequest(rc) => {
47+
write!(f, "subrequest failed with return code: {}", rc)
48+
}
49+
SampleHandlerError::Response(rc) => {
50+
write!(f, "response creation failed with return code: {}", rc)
51+
}
52+
}
53+
}
54+
}
55+
56+
impl IntoHandlerStatus for SampleHandlerError {
57+
fn into_handler_status(self, r: &Request) -> ngx_int_t {
58+
ngx_log_error!(NGX_LOG_ERR, r.log(), "subrequest example: {self}");
59+
Status::NGX_ERROR.into()
60+
}
61+
}
62+
63+
impl HttpRequestHandler for SampleHandler {
64+
const PHASE: HttpPhase = HttpPhase::Access;
65+
type Output = Result<Status, SampleHandlerError>;
66+
67+
fn handler(request: &mut Request) -> Self::Output {
68+
let co = Module::location_conf(request).expect("module config is none");
69+
ngx_log_debug_http!(request, "subrequest module enabled: {}", co.enable);
70+
71+
if co.enable != 1 {
72+
return Ok(Status::NGX_DECLINED);
73+
}
74+
75+
let rptr: *mut ngx_http_request_t = request.as_mut();
76+
77+
match SRCtx::get(request) {
78+
Some(ctx) => ctx.rc.map_or(
79+
// `ctx` has been created but not filled yet - subrequest is still in progress
80+
Ok(Status::NGX_AGAIN),
81+
// `ctx` has been created and filled - subrequest is completed
82+
|rc| {
83+
let status = ctx.status.0;
84+
let msg = format!("subrequest completed with HTTP status: {status}, rc: {rc}");
85+
ngx_log_debug_http!(request, "{msg}");
86+
87+
if status >= nginx_sys::NGX_HTTP_SPECIAL_RESPONSE as _ {
88+
Ok(Status::from(ctx.status))
89+
} else if rc == nginx_sys::NGX_OK as _ && ctx.out.is_some() {
90+
let outbuf = unsafe { &*ctx.out.unwrap().buf };
91+
let mut ct = ctx.ct;
92+
let mut cv: ngx_http_complex_value_t = unsafe { core::mem::zeroed() };
93+
cv.value = ngx_str_t {
94+
len: unsafe { outbuf.last.offset_from(outbuf.pos) } as _,
95+
data: outbuf.pos as _,
96+
};
97+
let resp_rc = unsafe {
98+
ngx_http_send_response(rptr, status, &raw mut ct, &raw mut cv)
99+
};
100+
if resp_rc == nginx_sys::NGX_OK as _ {
101+
Ok(Status::from(ctx.status))
102+
} else {
103+
Err(SampleHandlerError::Response(resp_rc))
104+
}
105+
} else if rc == nginx_sys::NGX_OK as _ {
106+
Ok(Status::from(ctx.status))
107+
} else if let Ok(http_status) = HTTPStatus::try_from(rc) {
108+
Ok(Status::from(http_status))
109+
} else {
110+
Err(SampleHandlerError::SubRequest(rc))
111+
}
112+
},
113+
),
114+
None => {
115+
if SRCtx::create(request).is_some() {
116+
let uri: &str = co.uri.to_str().unwrap_or("/proxy");
117+
118+
SubRequestBuilder::new(request, uri)?
119+
.args("arg1=val1&arg2=val2")?
120+
.init(|sr| {
121+
ngx_log_debug_http!(
122+
sr,
123+
"initializing subrequest with URI: {}",
124+
sr.path()
125+
);
126+
sr.init_headers_in(8).ok_or("cannot initialize headers")?;
127+
sr.discard_request_body();
128+
sr.add_header_in("X-SubRequest", "1").ok_or("cannot add header")
129+
})
130+
.handler(sr_handler)
131+
.in_memory()
132+
.waited()
133+
.build()?;
134+
135+
Ok(Status::NGX_AGAIN)
136+
} else {
137+
Err(SampleHandlerError::ContextAllocation)
138+
}
139+
}
140+
}
141+
}
142+
}
143+
144+
struct SRCtx<'r> {
145+
rc: Option<ngx_int_t>,
146+
status: HTTPStatus,
147+
out: Option<&'r nginx_sys::ngx_chain_t>,
148+
ct: ngx_str_t,
149+
}
150+
151+
impl SRCtx<'_> {
152+
fn create(request: &mut Request) -> Option<&mut Self> {
153+
let ctx_ref = unsafe { request.pool().allocate_with_cleanup(Self::default).ok()?.as_mut() };
154+
request.set_module_ctx(ctx_ref as *mut _ as _, Module::module());
155+
Some(ctx_ref)
156+
}
157+
158+
fn get(request: &Request) -> Option<&Self> {
159+
request.get_module_ctx::<Self>(Module::module())
160+
}
161+
162+
fn get_mut(request: &mut Request) -> Option<&mut Self> {
163+
request.get_module_ctx_mut::<Self>(Module::module())
164+
}
165+
}
166+
167+
impl Default for SRCtx<'_> {
168+
fn default() -> Self {
169+
Self { rc: None, status: HTTPStatus(NGX_ERROR as _), out: None, ct: ngx_str_t::empty() }
170+
}
171+
}
172+
173+
fn sr_handler(r: &mut Request, mut rc: ngx_int_t) -> ngx_int_t {
174+
let newctx = SRCtx {
175+
rc: Some(rc),
176+
status: r.status(),
177+
// SAFETY: `r.as_ref().out` is valid as long as the main request is not finalized,
178+
// and the subrequest is always finalized before the main request.
179+
out: core::ptr::NonNull::new(r.as_ref().out).map(|out| unsafe { out.as_ref() }),
180+
ct: r.as_ref().headers_out.content_type,
181+
};
182+
if let Some(ctx) = SRCtx::get_mut(r.main_mut()) {
183+
*ctx = newctx;
184+
} else {
185+
ngx_log_error!(nginx_sys::NGX_LOG_ERR, r.log(), "subrequest: context not found");
186+
rc = NGX_ERROR as _;
187+
}
188+
rc
189+
}
190+
191+
static NGX_HTTP_SUBREQUEST_MODULE_CTX: ngx_http_module_t = ngx_http_module_t {
192+
preconfiguration: None,
193+
postconfiguration: Some(Module::postconfiguration),
194+
create_main_conf: None,
195+
init_main_conf: None,
196+
create_srv_conf: None,
197+
merge_srv_conf: None,
198+
create_loc_conf: Some(Module::create_loc_conf),
199+
merge_loc_conf: Some(Module::merge_loc_conf),
200+
};
201+
202+
#[cfg(feature = "export-modules")]
203+
ngx::ngx_modules!(ngx_http_subrequest_module);
204+
205+
#[used]
206+
#[allow(non_upper_case_globals)]
207+
#[cfg_attr(not(feature = "export-modules"), unsafe(no_mangle))]
208+
pub static mut ngx_http_subrequest_module: ngx_module_t = ngx_module_t {
209+
ctx: &raw const NGX_HTTP_SUBREQUEST_MODULE_CTX as _,
210+
commands: unsafe { &raw mut NGX_HTTP_SUBREQUEST_COMMANDS[0] },
211+
type_: nginx_sys::NGX_HTTP_MODULE as _,
212+
..ngx_module_t::default()
213+
};
214+
215+
struct Module;
216+
217+
impl HttpModule for Module {
218+
fn module() -> &'static ngx_module_t {
219+
unsafe { &*::core::ptr::addr_of!(ngx_http_subrequest_module) }
220+
}
221+
222+
unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t {
223+
// SAFETY: this function is called with non-NULL cf always
224+
let cf = unsafe { &mut *cf };
225+
add_phase_handler::<SampleHandler>(cf)
226+
.map_or(nginx_sys::NGX_ERROR as _, |_| nginx_sys::NGX_OK as _)
227+
}
228+
}
229+
230+
#[derive(Debug)]
231+
struct ModuleConfig {
232+
enable: ngx_flag_t,
233+
uri: ngx_str_t,
234+
}
235+
236+
impl Default for ModuleConfig {
237+
fn default() -> Self {
238+
Self { enable: NGX_CONF_UNSET_FLAG, uri: ngx_str_t::empty() }
239+
}
240+
}
241+
242+
impl Merge for ModuleConfig {
243+
fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> {
244+
if self.enable == NGX_CONF_UNSET_FLAG {
245+
if prev.enable != NGX_CONF_UNSET_FLAG {
246+
self.enable = prev.enable;
247+
} else {
248+
self.enable = 0;
249+
}
250+
}
251+
if self.uri.data.is_null() {
252+
self.uri = prev.uri;
253+
}
254+
if self.enable == 1 && self.uri.data.is_null() {
255+
self.uri = ngx::ngx_string!("/proxy");
256+
}
257+
Ok(())
258+
}
259+
}
260+
261+
unsafe impl HttpModuleLocationConf for Module {
262+
type LocationConf = ModuleConfig;
263+
}
264+
265+
static mut NGX_HTTP_SUBREQUEST_COMMANDS: [ngx_command_t; 3] = [
266+
ngx_command_t {
267+
name: ngx::ngx_string!("subrequest"),
268+
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
269+
set: Some(nginx_sys::ngx_conf_set_flag_slot),
270+
conf: NGX_HTTP_LOC_CONF_OFFSET,
271+
offset: core::mem::offset_of!(ModuleConfig, enable),
272+
post: core::ptr::null_mut(),
273+
},
274+
ngx_command_t {
275+
name: ngx::ngx_string!("subrequest_uri"),
276+
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
277+
set: Some(nginx_sys::ngx_conf_set_str_slot),
278+
conf: NGX_HTTP_LOC_CONF_OFFSET,
279+
offset: core::mem::offset_of!(ModuleConfig, uri),
280+
post: core::ptr::null_mut(),
281+
},
282+
ngx_command_t::empty(),
283+
];

examples/t/subrequest.t

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/perl
2+
3+
# (C) Nginx, Inc
4+
5+
# Tests for ngx-rust example modules.
6+
7+
###############################################################################
8+
9+
use warnings;
10+
use strict;
11+
12+
use Test::More;
13+
14+
BEGIN { use FindBin; chdir($FindBin::Bin); }
15+
16+
use lib 'lib';
17+
use Test::Nginx;
18+
19+
###############################################################################
20+
21+
select STDERR; $| = 1;
22+
select STDOUT; $| = 1;
23+
24+
my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(2)
25+
->write_file_expand('nginx.conf', <<'EOF');
26+
27+
%%TEST_GLOBALS%%
28+
29+
daemon off;
30+
31+
events {
32+
}
33+
34+
http {
35+
%%TEST_GLOBALS_HTTP%%
36+
37+
server {
38+
listen 127.0.0.1:8080;
39+
server_name localhost;
40+
41+
location / {
42+
subrequest on;
43+
}
44+
45+
location /non_existing {
46+
subrequest on;
47+
subrequest_uri /non_existing_upstream;
48+
}
49+
50+
location /proxy {
51+
internal;
52+
proxy_pass http://127.0.0.1:8081;
53+
}
54+
}
55+
56+
server {
57+
listen 127.0.0.1:8081;
58+
server_name localhost;
59+
60+
location / {
61+
set $reply 'Invalid';
62+
if ($http_x_subrequest) {
63+
set $reply 'Hello from backend';
64+
}
65+
return 200 $reply;
66+
}
67+
}
68+
}
69+
70+
EOF
71+
72+
$t->write_file('index.html', '');
73+
$t->run();
74+
75+
like(http_get('/'),
76+
qr/200 OK.*Hello from backend/s,
77+
'subrequest');
78+
like(http_get('/non_existing'), qr/404 Not Found/s,
79+
'subrequest to non-existing upstream');

0 commit comments

Comments
 (0)