featherbit/plugins/native/
request_size_limit.rs1use async_trait::async_trait;
7use bytes::Bytes;
8use std::collections::HashMap;
9
10use crate::context::{Context, GatewayError};
11use crate::plugins::{Plugin, PluginExecutionError, 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(
53 &self,
54 ctx: Context,
55 _named_inputs: &HashMap<String, serde_json::Value>,
56 ) -> PluginResult {
57 let body_len = ctx.request.body.len();
58 if body_len > self.max_bytes {
59 let mut ctx = ctx;
60 ctx.response.status_code = 413;
61 ctx.response.body = Bytes::from(
62 r#"{"error": "payload_too_large", "message": "Request body exceeds size limit"}"#,
63 );
64 ctx.response.headers.insert(
65 "content-type".to_string(),
66 vec!["application/json".to_string()],
67 );
68 return Err(PluginExecutionError {
69 context: ctx,
70 error: GatewayError {
71 node_id: String::new(),
72 code: "PAYLOAD_TOO_LARGE".to_string(),
73 message: format!("Body size {} exceeds limit {}", body_len, self.max_bytes),
74 metadata: HashMap::new(),
75 },
76 });
77 }
78
79 Ok(PluginOutput {
80 context: ctx,
81 named_outputs: HashMap::new(),
82 })
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
93 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
94
95 fn ctx(body: &[u8]) -> Context {
96 Context {
97 request: GatewayRequest {
98 method: "POST".to_string(),
99 path: "/hello".to_string(),
100 host: "h".to_string(),
101 scheme: "http".to_string(),
102 headers: HashMap::new(),
103 query_params: HashMap::new(),
104 body: Bytes::copy_from_slice(body),
105 remote_addr: "1.2.3.4:5".to_string(),
106 protocol: Protocol::Http1,
107 },
108 response: GatewayResponse {
109 status_code: 0,
110 headers: HashMap::new(),
111 body: Bytes::new(),
112 },
113 message: HashMap::new(),
114 errors: Vec::new(),
115 }
116 }
117
118 fn plugin(max_bytes: u64) -> RequestSizeLimitPlugin {
119 let mut map = HashMap::new();
120 map.insert("max_bytes".to_string(), serde_json::json!(max_bytes));
121 RequestSizeLimitPlugin::from_config(&map).unwrap()
122 }
123
124 #[tokio::test]
125 async fn test_body_under_limit_passes() {
126 let out = plugin(10).execute(ctx(b"12345"), &HashMap::new()).await;
127 assert!(out.is_ok());
128 }
129
130 #[tokio::test]
131 async fn test_body_at_limit_passes() {
132 let out = plugin(5).execute(ctx(b"12345"), &HashMap::new()).await;
134 assert!(out.is_ok());
135 }
136
137 #[tokio::test]
138 async fn test_body_over_limit_rejected_413() {
139 let err = plugin(5)
140 .execute(ctx(b"123456"), &HashMap::new())
141 .await
142 .unwrap_err();
143 assert_eq!(err.error.code, "PAYLOAD_TOO_LARGE");
144 assert_eq!(err.context.response.status_code, 413);
145 }
146
147 #[tokio::test]
148 async fn test_empty_body_passes() {
149 assert!(plugin(0).execute(ctx(b""), &HashMap::new()).await.is_ok());
150 }
151
152 #[test]
153 fn test_default_limit_is_1mib() {
154 let p = RequestSizeLimitPlugin::from_config(&HashMap::new()).unwrap();
155 assert_eq!(p.max_bytes, 1_048_576);
156 }
157}