Skip to content

Commit e63cec9

Browse files
committed
feat: subrequest support
1 parent 56c2bf4 commit e63cec9

9 files changed

Lines changed: 585 additions & 29 deletions

File tree

.github/workflows/nginx.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ env:
5353
load_module ${{ github.workspace }}/nginx/objs/ngx_http_awssigv4_module.so;
5454
load_module ${{ github.workspace }}/nginx/objs/ngx_http_curl_module.so;
5555
load_module ${{ github.workspace }}/nginx/objs/ngx_http_shared_dict_module.so;
56+
load_module ${{ github.workspace }}/nginx/objs/ngx_http_subrequest_module.so;
5657
load_module ${{ github.workspace }}/nginx/objs/ngx_http_upstream_custom_module.so;
5758
5859
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: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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, Merge,
7+
MergeConfigError, Request, RequestContext, 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_command_t,
13+
ngx_conf_t, ngx_flag_t, ngx_http_complex_value_t, ngx_http_module_t, ngx_http_request_t,
14+
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(SubRequestError),
24+
SubRequest(ngx_int_t),
25+
Response(ngx_int_t),
26+
}
27+
28+
impl Display for SampleHandlerError {
29+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30+
match self {
31+
SampleHandlerError::ContextAllocation => {
32+
write!(f, "context allocation failed")
33+
}
34+
SampleHandlerError::SubRequestCreation(e) => {
35+
write!(f, "subrequest creation failed: {}", e)
36+
}
37+
SampleHandlerError::SubRequest(rc) => {
38+
write!(f, "subrequest failed with return code: {}", rc)
39+
}
40+
SampleHandlerError::Response(rc) => {
41+
write!(f, "response creation failed with return code: {}", rc)
42+
}
43+
}
44+
}
45+
}
46+
47+
impl HttpRequestHandler for SampleHandler {
48+
const PHASE: HttpPhase = HttpPhase::Access;
49+
type Output = Result<Status, SampleHandlerError>;
50+
51+
fn handler(request: &mut Request) -> Self::Output {
52+
let co = Module::location_conf(request).expect("module config is none");
53+
ngx_log_debug_http!(request, "subrequest module enabled: {}", co.enable);
54+
55+
if co.enable != 1 {
56+
return Ok(Status::NGX_DECLINED);
57+
}
58+
59+
let request_ptr: *mut ngx_http_request_t = request.as_mut();
60+
61+
if let Some(ctx) = SRCtx::get(request) {
62+
if ctx.rc.is_none() {
63+
return Ok(Status::NGX_AGAIN);
64+
}
65+
66+
let rc = ctx.rc.unwrap();
67+
68+
let msg = format!(
69+
"subrequest completed with HTTP status: {}, rc: {}",
70+
ctx.status.0, rc
71+
);
72+
ngx_log_debug_http!(request, "{msg}");
73+
74+
if ctx.status.0 >= nginx_sys::NGX_HTTP_SPECIAL_RESPONSE as _ {
75+
return Ok(Status::from(ctx.status));
76+
}
77+
78+
if rc == nginx_sys::NGX_OK as _ {
79+
let mut cv: ngx_http_complex_value_t = unsafe { core::mem::zeroed() };
80+
cv.value = unsafe { ngx_str_t::from_str(request.pool().as_ptr(), &msg) };
81+
82+
let rc = unsafe {
83+
ngx_http_send_response(
84+
request_ptr,
85+
ctx.status.0 as _,
86+
core::ptr::null_mut(),
87+
&raw mut cv,
88+
)
89+
};
90+
91+
if rc == nginx_sys::NGX_OK as _ {
92+
Ok(Status::from(ctx.status))
93+
} else {
94+
Err(SampleHandlerError::Response(rc))
95+
}
96+
} else {
97+
Err(SampleHandlerError::SubRequest(rc))
98+
}
99+
} else if SRCtx::create(request, SRCtx::default).is_some() {
100+
let uri: &str = if co.uri.is_empty() {
101+
"/proxy"
102+
} else {
103+
co.uri.to_str().unwrap_or("/proxy")
104+
};
105+
106+
SubRequestBuilder::new(uri)
107+
.args("arg1=val1&arg2=val2")
108+
.in_memory()
109+
.waited()
110+
.build(request, sr_handler)
111+
.map_err(SampleHandlerError::SubRequestCreation)?;
112+
113+
Ok(Status::NGX_AGAIN)
114+
} else {
115+
Err(SampleHandlerError::ContextAllocation)
116+
}
117+
}
118+
}
119+
120+
struct SRCtx {
121+
rc: Option<ngx_int_t>,
122+
status: HTTPStatus,
123+
}
124+
125+
impl Default for SRCtx {
126+
fn default() -> Self {
127+
Self {
128+
rc: None,
129+
status: HTTPStatus(NGX_ERROR as _),
130+
}
131+
}
132+
}
133+
134+
impl RequestContext<Module> for SRCtx {}
135+
136+
fn sr_handler(r: &mut Request, mut rc: ngx_int_t) -> ngx_int_t {
137+
let status = r.get_status();
138+
if let Some(ctx) = SRCtx::get_mut(r.get_main_mut()) {
139+
ctx.rc = Some(rc);
140+
ctx.status = status;
141+
} else {
142+
ngx_log_error!(
143+
nginx_sys::NGX_LOG_ERR,
144+
r.log(),
145+
"subrequest: context not found"
146+
);
147+
rc = NGX_ERROR as _;
148+
}
149+
rc
150+
}
151+
152+
static NGX_HTTP_SUBREQUEST_MODULE_CTX: ngx_http_module_t = ngx_http_module_t {
153+
preconfiguration: None,
154+
postconfiguration: Some(Module::postconfiguration),
155+
create_main_conf: None,
156+
init_main_conf: None,
157+
create_srv_conf: None,
158+
merge_srv_conf: None,
159+
create_loc_conf: Some(Module::create_loc_conf),
160+
merge_loc_conf: Some(Module::merge_loc_conf),
161+
};
162+
163+
#[cfg(feature = "export-modules")]
164+
ngx::ngx_modules!(ngx_http_subrequest_module);
165+
166+
#[used]
167+
#[allow(non_upper_case_globals)]
168+
#[cfg_attr(not(feature = "export-modules"), unsafe(no_mangle))]
169+
pub static mut ngx_http_subrequest_module: ngx_module_t = ngx_module_t {
170+
ctx: &raw const NGX_HTTP_SUBREQUEST_MODULE_CTX as _,
171+
commands: unsafe { &raw mut NGX_HTTP_SUBREQUEST_COMMANDS[0] },
172+
type_: nginx_sys::NGX_HTTP_MODULE as _,
173+
..ngx_module_t::default()
174+
};
175+
176+
struct Module;
177+
178+
impl HttpModule for Module {
179+
fn module() -> &'static ngx_module_t {
180+
unsafe { &*::core::ptr::addr_of!(ngx_http_subrequest_module) }
181+
}
182+
183+
unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t {
184+
// SAFETY: this function is called with non-NULL cf always
185+
let cf = unsafe { &mut *cf };
186+
add_phase_handler::<SampleHandler>(cf)
187+
.map_or(nginx_sys::NGX_ERROR as _, |_| nginx_sys::NGX_OK as _)
188+
}
189+
}
190+
191+
#[derive(Debug)]
192+
struct ModuleConfig {
193+
enable: ngx_flag_t,
194+
uri: ngx_str_t,
195+
}
196+
197+
impl Default for ModuleConfig {
198+
fn default() -> Self {
199+
Self {
200+
enable: NGX_CONF_UNSET_FLAG,
201+
uri: ngx_str_t::empty(),
202+
}
203+
}
204+
}
205+
206+
impl Merge for ModuleConfig {
207+
fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> {
208+
if self.enable == NGX_CONF_UNSET_FLAG {
209+
if prev.enable != NGX_CONF_UNSET_FLAG {
210+
self.enable = prev.enable;
211+
} else {
212+
self.enable = 0;
213+
}
214+
}
215+
if self.uri.len == 0 {
216+
self.uri = prev.uri;
217+
}
218+
Ok(())
219+
}
220+
}
221+
222+
unsafe impl HttpModuleLocationConf for Module {
223+
type LocationConf = ModuleConfig;
224+
}
225+
226+
static mut NGX_HTTP_SUBREQUEST_COMMANDS: [ngx_command_t; 3] = [
227+
ngx_command_t {
228+
name: ngx::ngx_string!("subrequest"),
229+
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
230+
set: Some(nginx_sys::ngx_conf_set_flag_slot),
231+
conf: NGX_HTTP_LOC_CONF_OFFSET,
232+
offset: core::mem::offset_of!(ModuleConfig, enable),
233+
post: core::ptr::null_mut(),
234+
},
235+
ngx_command_t {
236+
name: ngx::ngx_string!("subrequest_uri"),
237+
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
238+
set: Some(nginx_sys::ngx_conf_set_str_slot),
239+
conf: NGX_HTTP_LOC_CONF_OFFSET,
240+
offset: core::mem::offset_of!(ModuleConfig, uri),
241+
post: core::ptr::null_mut(),
242+
},
243+
ngx_command_t::empty(),
244+
];

examples/t/subrequest.t

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
subrequest_uri /proxy;
44+
}
45+
46+
location /non_existing {
47+
subrequest on;
48+
subrequest_uri /non_existing_upstream;
49+
}
50+
51+
location /proxy {
52+
internal;
53+
proxy_pass http://127.0.0.1:8081;
54+
}
55+
}
56+
57+
server {
58+
listen 127.0.0.1:8081;
59+
server_name localhost;
60+
61+
location / {
62+
return 200 'Hello from backend';
63+
}
64+
}
65+
}
66+
67+
EOF
68+
69+
$t->write_file('index.html', '');
70+
$t->run();
71+
72+
like(http_get('/'),
73+
qr/200 OK.*subrequest completed with HTTP status: 200, rc: 0/s,
74+
'subrequest');
75+
like(http_get('/non_existing'), qr/404 Not Found/s,
76+
'subrequest to non-existing upstream');

src/http/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ mod request_context;
55
mod status;
66
mod upstream;
77

8+
/// HTTP subrequest builder and handler.
9+
#[cfg(feature = "alloc")]
10+
pub mod subrequest;
11+
812
pub use conf::*;
913
pub use module::*;
1014
pub use request::*;

0 commit comments

Comments
 (0)