featherbit/plugins/native/
request_size_limit.rs1use async_trait::async_trait;
7use bytes::Bytes;
8use std::collections::HashMap;
9
10use crate::context::Context;
11use crate::plugins::{Plugin, PluginOutput, PluginResult};
12
13pub struct RequestSizeLimitPlugin {
20 max_bytes: usize,
22}
23
24impl RequestSizeLimitPlugin {
25 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
37 let max_bytes = config
38 .get("max_bytes")
39 .and_then(|v| v.as_u64())
40 .unwrap_or(1_048_576) as usize; Ok(Self { max_bytes })
43 }
44}
45
46#[async_trait]
47impl Plugin for RequestSizeLimitPlugin {
48 fn plugin_type(&self) -> &str {
49 "request-size-limit"
50 }
51
52 async fn execute(&self, ctx: Context) -> PluginResult {
53 let body_len = ctx.request.body.len();
54 if body_len > self.max_bytes {
55 let mut ctx = ctx;
56 ctx.response.status_code = 413;
57 ctx.response.body = Bytes::from(
58 r#"{"error": "payload_too_large", "message": "Request body exceeds size limit"}"#,
59 );
60 ctx.response.headers.insert(
61 "content-type".to_string(),
62 vec!["application/json".to_string()],
63 );
64 return Ok(PluginOutput::on_port(ctx, "denied"));
65 }
66
67 Ok(PluginOutput::success(ctx))
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
78 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
79
80 fn ctx(body: &[u8]) -> Context {
81 Context {
82 request: GatewayRequest {
83 method: "POST".to_string(),
84 path: "/hello".to_string(),
85 host: "h".to_string(),
86 scheme: "http".to_string(),
87 headers: HashMap::new(),
88 query_params: HashMap::new(),
89 body: Bytes::copy_from_slice(body),
90 remote_addr: "1.2.3.4:5".to_string(),
91 protocol: Protocol::Http1,
92 },
93 response: GatewayResponse {
94 status_code: 0,
95 headers: HashMap::new(),
96 body: Bytes::new(),
97 stream: None,
98 },
99 message: HashMap::new(),
100 errors: Vec::new(),
101 }
102 }
103
104 fn plugin(max_bytes: u64) -> RequestSizeLimitPlugin {
105 let mut map = HashMap::new();
106 map.insert("max_bytes".to_string(), serde_json::json!(max_bytes));
107 RequestSizeLimitPlugin::from_config(&map).unwrap()
108 }
109
110 #[tokio::test]
111 async fn test_body_under_limit_passes() {
112 let out = plugin(10).execute(ctx(b"12345")).await.unwrap();
113 assert!(out.port.is_none());
114 }
115
116 #[tokio::test]
117 async fn test_body_at_limit_passes() {
118 let out = plugin(5).execute(ctx(b"12345")).await.unwrap();
120 assert!(out.port.is_none());
121 }
122
123 #[tokio::test]
124 async fn test_body_over_limit_rejected_413() {
125 let out = plugin(5).execute(ctx(b"123456")).await.unwrap();
126 assert_eq!(out.port, Some("denied"));
127 assert_eq!(out.context.response.status_code, 413);
128 }
129
130 #[tokio::test]
131 async fn test_empty_body_passes() {
132 assert!(plugin(0).execute(ctx(b"")).await.unwrap().port.is_none());
133 }
134
135 #[test]
136 fn test_default_limit_is_1mib() {
137 let p = RequestSizeLimitPlugin::from_config(&HashMap::new()).unwrap();
138 assert_eq!(p.max_bytes, 1_048_576);
139 }
140}