itsi 0.2.21.rc1 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/CHANGELOG.md +3 -0
  5. data/Cargo.lock +14 -14
  6. data/Dockerfile +9 -0
  7. data/crates/itsi_acme/src/caches/no.rs +1 -1
  8. data/crates/itsi_acme/src/caches/test.rs +3 -3
  9. data/crates/itsi_acme/src/config.rs +6 -6
  10. data/crates/itsi_acme/src/lib.rs +1 -1
  11. data/crates/itsi_error/Cargo.toml +1 -1
  12. data/crates/itsi_error/src/lib.rs +32 -15
  13. data/crates/itsi_rb_helpers/Cargo.toml +2 -2
  14. data/crates/itsi_rb_helpers/src/heap_value.rs +4 -4
  15. data/crates/itsi_rb_helpers/src/lib.rs +9 -5
  16. data/crates/itsi_scheduler/Cargo.toml +3 -3
  17. data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  18. data/crates/itsi_server/Cargo.toml +3 -3
  19. data/crates/itsi_server/src/lib.rs +3 -2
  20. data/crates/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +10 -3
  21. data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +10 -6
  22. data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +7 -5
  23. data/crates/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +2 -2
  24. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +10 -7
  25. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +13 -5
  26. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +6 -6
  27. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +15 -11
  28. data/crates/itsi_server/src/ruby_types/itsi_server.rs +34 -18
  29. data/crates/itsi_server/src/server/frame_stream.rs +2 -1
  30. data/crates/itsi_server/src/server/middleware_stack/middleware.rs +1 -1
  31. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +1 -3
  32. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +8 -2
  33. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +2 -2
  34. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +1 -1
  35. data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +17 -7
  36. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +90 -21
  37. data/crates/itsi_server/src/server/middleware_stack/mod.rs +12 -12
  38. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +4 -3
  39. data/crates/itsi_server/src/services/password_hasher.rs +8 -2
  40. data/crates/itsi_server/src/services/rate_limiter.rs +72 -25
  41. data/crates/itsi_server/src/services/static_file_server.rs +38 -13
  42. data/crates/itsi_tracing/src/lib.rs +3 -3
  43. data/gems/scheduler/Cargo.lock +3997 -541
  44. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  45. data/gems/server/Cargo.lock +40 -13
  46. data/gems/server/lib/itsi/http_request.rb +22 -17
  47. data/gems/server/lib/itsi/rack_env_pool.rb +7 -17
  48. data/gems/server/lib/itsi/server/config/middleware/static_assets.rb +8 -1
  49. data/gems/server/lib/itsi/server/rack_interface.rb +12 -0
  50. data/gems/server/lib/itsi/server/version.rb +1 -1
  51. data/lib/itsi/version.rb +1 -1
  52. data/mise.toml +2 -0
  53. metadata +9 -5
@@ -63,7 +63,7 @@ pub struct ResponseInner {
63
63
 
64
64
  #[derive(Debug)]
65
65
  pub enum ResponseFrame {
66
- HttpResponse(HttpResponse),
66
+ HttpResponse(Box<HttpResponse>),
67
67
  HijackedResponse(ItsiHttpResponse),
68
68
  }
69
69
 
@@ -225,7 +225,9 @@ impl ItsiHttpResponse {
225
225
  if let Some(mut response) = self.response.write().take() {
226
226
  *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
227
227
  if let Some(sender) = self.response_sender.write().take() {
228
- sender.send(ResponseFrame::HttpResponse(response)).ok();
228
+ sender
229
+ .send(ResponseFrame::HttpResponse(Box::new(response)))
230
+ .ok();
229
231
  }
230
232
  }
231
233
  }
@@ -235,7 +237,9 @@ impl ItsiHttpResponse {
235
237
  if let Some(mut response) = self.response.write().take() {
236
238
  *response.status_mut() = StatusCode::SERVICE_UNAVAILABLE;
237
239
  if let Some(sender) = self.response_sender.write().take() {
238
- sender.send(ResponseFrame::HttpResponse(response)).ok();
240
+ sender
241
+ .send(ResponseFrame::HttpResponse(Box::new(response)))
242
+ .ok();
239
243
  }
240
244
  }
241
245
  }
@@ -253,7 +257,9 @@ impl ItsiHttpResponse {
253
257
  *response.body_mut() = HttpBody::stream(buffered);
254
258
  self.frame_writer.write().replace(writer);
255
259
  if let Some(sender) = self.response_sender.write().take() {
256
- sender.send(ResponseFrame::HttpResponse(response)).ok();
260
+ sender
261
+ .send(ResponseFrame::HttpResponse(Box::new(response)))
262
+ .ok();
257
263
  }
258
264
  } else {
259
265
  info!("No response!");
@@ -280,7 +286,9 @@ impl ItsiHttpResponse {
280
286
  *response.body_mut() = HttpBody::full(frame);
281
287
  }
282
288
  if let Some(sender) = self.response_sender.write().take() {
283
- sender.send(ResponseFrame::HttpResponse(response)).ok();
289
+ sender
290
+ .send(ResponseFrame::HttpResponse(Box::new(response)))
291
+ .ok();
284
292
  }
285
293
  }
286
294
 
@@ -110,7 +110,7 @@ pub fn send_watcher_command(fd: &OwnedFd, cmd: WatcherCommand) -> Result<()> {
110
110
  match write(fd, &buf) {
111
111
  Ok(_) => Ok(()),
112
112
  Err(e) => Err(magnus::Error::new(
113
- magnus::exception::standard_error(),
113
+ magnus::Ruby::get().unwrap().exception_standard_error(),
114
114
  format!("Failed to send command to watcher: {}", e),
115
115
  )),
116
116
  }
@@ -122,14 +122,14 @@ pub fn watch_groups(
122
122
  // Create bidirectional pipes for communication
123
123
  let (parent_read_fd, child_write_fd): (OwnedFd, OwnedFd) = pipe().map_err(|e| {
124
124
  magnus::Error::new(
125
- magnus::exception::standard_error(),
125
+ magnus::Ruby::get().unwrap().exception_standard_error(),
126
126
  format!("Failed to create parent read pipe: {}", e),
127
127
  )
128
128
  })?;
129
129
 
130
130
  let (child_read_fd, parent_write_fd): (OwnedFd, OwnedFd) = pipe().map_err(|e| {
131
131
  magnus::Error::new(
132
- magnus::exception::standard_error(),
132
+ magnus::Ruby::get().unwrap().exception_standard_error(),
133
133
  format!("Failed to create child read pipe: {}", e),
134
134
  )
135
135
  })?;
@@ -137,7 +137,7 @@ pub fn watch_groups(
137
137
  let fork_result = unsafe {
138
138
  fork().map_err(|e| {
139
139
  magnus::Error::new(
140
- magnus::exception::standard_error(),
140
+ magnus::Ruby::get().unwrap().exception_standard_error(),
141
141
  format!("Failed to fork file watcher: {}", e),
142
142
  )
143
143
  })
@@ -194,7 +194,7 @@ pub fn watch_groups(
194
194
 
195
195
  let glob = Glob::new(&remaining_pattern).map_err(|e| {
196
196
  magnus::Error::new(
197
- magnus::exception::standard_error(),
197
+ magnus::Ruby::get().unwrap().exception_standard_error(),
198
198
  format!(
199
199
  "Failed to create watch glob for pattern '{}': {}",
200
200
  remaining_pattern, e
@@ -203,7 +203,7 @@ pub fn watch_groups(
203
203
  })?;
204
204
  let glob_set = GlobSetBuilder::new().add(glob).build().map_err(|e| {
205
205
  magnus::Error::new(
206
- magnus::exception::standard_error(),
206
+ magnus::Ruby::get().unwrap().exception_standard_error(),
207
207
  format!("Failed to create watch glob set: {}", e),
208
208
  )
209
209
  })?;
@@ -14,7 +14,7 @@ use magnus::{
14
14
  block::Proc,
15
15
  error::Result,
16
16
  value::{LazyId, ReprValue},
17
- RArray, RHash, Ruby, Symbol, TryConvert, Value,
17
+ RArray, RHash, Ruby, TryConvert, Value,
18
18
  };
19
19
  use nix::{
20
20
  fcntl::{fcntl, FcntlArg, FdFlag},
@@ -161,14 +161,14 @@ impl ServerParams {
161
161
  Vec::<String>::try_convert(error_lines.unwrap().as_value())?;
162
162
  ItsiServerConfig::print_config_errors(errors);
163
163
  return Err(magnus::Error::new(
164
- magnus::exception::runtime_error(),
164
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
165
165
  "Failed to set middleware",
166
166
  ));
167
167
  }
168
168
  let middleware = MiddlewareSet::new(routes_raw)?;
169
169
  self.middleware.set(middleware).map_err(|_| {
170
170
  magnus::Error::new(
171
- magnus::exception::runtime_error(),
171
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
172
172
  "Failed to set middleware",
173
173
  )
174
174
  })?;
@@ -361,7 +361,7 @@ impl ServerParams {
361
361
  let bind_to_fd_map: HashMap<String, i32> = serde_json::from_str(preexisting_listeners)
362
362
  .map_err(|e| {
363
363
  magnus::Error::new(
364
- magnus::exception::standard_error(),
364
+ magnus::Ruby::get().unwrap().exception_standard_error(),
365
365
  format!("Invalid listener info: {}", e),
366
366
  )
367
367
  })?;
@@ -393,7 +393,10 @@ impl ServerParams {
393
393
  .iter()
394
394
  .map(|listener| {
395
395
  listener.handover().map_err(|e| {
396
- magnus::Error::new(magnus::exception::runtime_error(), e.to_string())
396
+ magnus::Error::new(
397
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
398
+ e.to_string(),
399
+ )
397
400
  })
398
401
  })
399
402
  .collect::<Result<HashMap<String, i32>>>()?;
@@ -419,7 +422,8 @@ impl ItsiServerConfig {
419
422
  itsi_config_proc.clone(),
420
423
  ) {
421
424
  Ok(server_params) => {
422
- cli_params.delete::<_, Value>(Symbol::new("listeners"))?;
425
+ cli_params
426
+ .delete::<_, Value>(magnus::Ruby::get().unwrap().to_symbol("listeners"))?;
423
427
 
424
428
  let watcher_fd = if let Some(watchers) = server_params.notify_watchers.clone() {
425
429
  file_watcher::watch_groups(watchers)?
@@ -436,7 +440,7 @@ impl ItsiServerConfig {
436
440
  })
437
441
  }
438
442
  Err(err) => Err(magnus::Error::new(
439
- magnus::exception::standard_error(),
443
+ magnus::Ruby::get().unwrap().exception_standard_error(),
440
444
  format!("Error loading initial configuration {:?}", err),
441
445
  )),
442
446
  }
@@ -493,7 +497,7 @@ impl ItsiServerConfig {
493
497
  if !errors.is_empty() {
494
498
  Self::print_config_errors(errors);
495
499
  return Err(magnus::Error::new(
496
- magnus::exception::standard_error(),
500
+ magnus::Ruby::get().unwrap().exception_standard_error(),
497
501
  "Invalid server config",
498
502
  ));
499
503
  }
@@ -567,13 +571,13 @@ impl ItsiServerConfig {
567
571
  .map(|(str, fd)| {
568
572
  let dupped_fd = dup(*fd).map_err(|errno| {
569
573
  magnus::Error::new(
570
- magnus::exception::standard_error(),
574
+ magnus::Ruby::get().unwrap().exception_standard_error(),
571
575
  format!("Errno {} while trying to dup {}", errno, fd),
572
576
  )
573
577
  })?;
574
578
  Self::clear_cloexec(dupped_fd).map_err(|e| {
575
579
  magnus::Error::new(
576
- magnus::exception::standard_error(),
580
+ magnus::Ruby::get().unwrap().exception_standard_error(),
577
581
  format!("Failed to clear cloexec flag for fd {}: {}", dupped_fd, e),
578
582
  )
579
583
  })?;
@@ -629,7 +633,7 @@ impl ItsiServerConfig {
629
633
  serde_json::to_string(&self.server_params.read().listener_info.lock().clone())
630
634
  .map_err(|e| {
631
635
  magnus::Error::new(
632
- magnus::exception::standard_error(),
636
+ magnus::Ruby::get().unwrap().exception_standard_error(),
633
637
  format!("Invalid listener info: {}", e),
634
638
  )
635
639
  })?;
@@ -13,26 +13,32 @@ use tracing::{info, instrument};
13
13
  mod file_watcher;
14
14
  pub mod itsi_server_config;
15
15
  #[magnus::wrap(class = "Itsi::Server", free_immediately, size)]
16
- #[derive(Clone)]
16
+ #[derive(Clone, Default)]
17
17
  pub struct ItsiServer {
18
- pub config: Arc<Mutex<Arc<ItsiServerConfig>>>,
18
+ pub config: Arc<Mutex<Option<Arc<ItsiServerConfig>>>>,
19
19
  }
20
20
 
21
21
  impl ItsiServer {
22
- pub fn new(
23
- ruby: &Ruby,
22
+ pub fn initialize(
23
+ &self,
24
24
  cli_params: RHash,
25
25
  itsifile_path: Option<PathBuf>,
26
26
  itsi_config_proc: Option<Proc>,
27
- ) -> Result<Self> {
28
- Ok(Self {
29
- config: Arc::new(Mutex::new(Arc::new(ItsiServerConfig::new(
30
- ruby,
31
- cli_params,
32
- itsifile_path,
33
- itsi_config_proc,
34
- )?))),
35
- })
27
+ ) -> Result<()> {
28
+ let ruby = Ruby::get().map_err(|_| {
29
+ magnus::Error::new(
30
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
31
+ "Failed to acquire Ruby VM handle",
32
+ )
33
+ })?;
34
+ let config = Arc::new(ItsiServerConfig::new(
35
+ &ruby,
36
+ cli_params,
37
+ itsifile_path,
38
+ itsi_config_proc,
39
+ )?);
40
+ *self.config.lock() = Some(config);
41
+ Ok(())
36
42
  }
37
43
 
38
44
  pub fn stop(&self) -> Result<()> {
@@ -40,10 +46,20 @@ impl ItsiServer {
40
46
  Ok(())
41
47
  }
42
48
 
49
+ fn config(&self) -> Result<Arc<ItsiServerConfig>> {
50
+ self.config.lock().as_ref().cloned().ok_or_else(|| {
51
+ magnus::Error::new(
52
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
53
+ "Itsi::Server not initialized",
54
+ )
55
+ })
56
+ }
57
+
43
58
  #[instrument(skip(self))]
44
59
  pub fn start(&self) -> Result<()> {
45
- self.config.lock().server_params.read().setup_listeners()?;
46
- let result = if self.config.lock().server_params.read().silence {
60
+ let server_config = self.config()?;
61
+ server_config.server_params.read().setup_listeners()?;
62
+ let result = if server_config.server_params.read().silence {
47
63
  run_silently(|| self.build_and_run_strategy())
48
64
  } else {
49
65
  info!("Itsi - Rolling into action. ⚪💨");
@@ -60,11 +76,11 @@ impl ItsiServer {
60
76
  }
61
77
 
62
78
  pub(crate) fn build_strategy(&self) -> Result<ServeStrategy> {
63
- let server_config = self.config.lock();
79
+ let server_config = self.config()?;
64
80
  Ok(if server_config.server_params.read().workers > 1 {
65
- ServeStrategy::Cluster(Arc::new(ClusterMode::new(server_config.clone())))
81
+ ServeStrategy::Cluster(Arc::new(ClusterMode::new(server_config)))
66
82
  } else {
67
- ServeStrategy::Single(Arc::new(SingleMode::new(server_config.clone(), 0)?))
83
+ ServeStrategy::Single(Arc::new(SingleMode::new(server_config, 0)?))
68
84
  })
69
85
  }
70
86
 
@@ -48,9 +48,10 @@ impl Stream for FrameStream {
48
48
  if this.shutdown_rx.has_changed().unwrap_or(false)
49
49
  && *this.shutdown_rx.borrow() == RunningPhase::ShutdownPending
50
50
  {
51
- while let Ok(bytes) = this.receiver.try_recv() {
51
+ if let Ok(bytes) = this.receiver.try_recv() {
52
52
  return Poll::Ready(Some(Ok(bytes)));
53
53
  }
54
+
54
55
  this.drained = true;
55
56
  return Poll::Ready(None);
56
57
  }
@@ -159,7 +159,7 @@ impl Eq for Middleware {}
159
159
 
160
160
  impl PartialOrd for Middleware {
161
161
  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
162
- Some(self.variant_order().cmp(&other.variant_order()))
162
+ Some(self.cmp(other))
163
163
  }
164
164
  }
165
165
 
@@ -293,9 +293,7 @@ impl MiddlewareLayer for Compression {
293
293
  };
294
294
  HttpBody::full(Bytes::from(compressed_bytes))
295
295
  } else {
296
- let stream = body
297
- .into_data_stream()
298
- .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e));
296
+ let stream = body.into_data_stream().map_err(std::io::Error::other);
299
297
  let async_read_fut = StreamReader::new(stream);
300
298
  let reader = BufReader::new(async_read_fut);
301
299
  match compression_method {
@@ -43,9 +43,9 @@ pub use error_response::ErrorResponse;
43
43
  pub use etag::ETag;
44
44
  pub use intrusion_protection::IntrusionProtection;
45
45
  pub use log_requests::LogRequests;
46
- use magnus::error::Result;
47
46
  use magnus::rb_sys::AsRawValue;
48
47
  use magnus::Value;
48
+ use magnus::{error::Result, Ruby};
49
49
  pub use max_body::MaxBody;
50
50
  pub use proxy::Proxy;
51
51
  pub use rate_limit::RateLimit;
@@ -88,7 +88,13 @@ pub trait FromValue: Sized + Send + Sync + 'static {
88
88
  }
89
89
  }
90
90
 
91
- let deserialized: Arc<Self> = Arc::new(deserialize(value)?);
91
+ let ruby = Ruby::get().map_err(|_| {
92
+ magnus::Error::new(
93
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
94
+ "Failed to acquire Ruby VM handle",
95
+ )
96
+ })?;
97
+ let deserialized: Arc<Self> = Arc::new(deserialize(&ruby, value)?);
92
98
  cache.insert(raw, deserialized.clone());
93
99
  Ok(deserialized)
94
100
  }
@@ -280,14 +280,14 @@ impl MiddlewareLayer for Proxy {
280
280
  .build()
281
281
  .map_err(|e| {
282
282
  magnus::Error::new(
283
- magnus::exception::runtime_error(),
283
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
284
284
  format!("Failed to build Reqwest client: {}", e),
285
285
  )
286
286
  })?,
287
287
  )
288
288
  .map_err(|_e| {
289
289
  magnus::Error::new(
290
- magnus::exception::standard_error(),
290
+ magnus::Ruby::get().unwrap().exception_standard_error(),
291
291
  "Failed to save resolver backends",
292
292
  )
293
293
  })?;
@@ -42,7 +42,7 @@ impl Redirect {
42
42
  *response.status_mut() = self.redirect_type.status_code();
43
43
  let destination = self.to.rewrite_request(req, context).parse().map_err(|e| {
44
44
  magnus::Error::new(
45
- magnus::exception::standard_error(),
45
+ magnus::Ruby::get().unwrap().exception_standard_error(),
46
46
  format!("Invalid Rewrite String: {:?}: {}", self.to, e),
47
47
  )
48
48
  })?;
@@ -8,7 +8,7 @@ use async_trait::async_trait;
8
8
  use derive_more::Debug;
9
9
  use either::Either;
10
10
  use itsi_rb_helpers::{HeapVal, HeapValue};
11
- use magnus::{block::Proc, error::Result, value::ReprValue, Symbol};
11
+ use magnus::{block::Proc, error::Result, value::ReprValue};
12
12
  use regex::Regex;
13
13
  use std::str::FromStr;
14
14
  use std::sync::atomic::Ordering;
@@ -44,23 +44,33 @@ impl FromStr for RequestType {
44
44
 
45
45
  impl RubyApp {
46
46
  pub fn from_value(params: HeapVal) -> magnus::error::Result<Arc<Self>> {
47
- let app = params.funcall::<_, _, Proc>(Symbol::new("[]"), ("app_proc",))?;
47
+ let app = params
48
+ .funcall::<_, _, Proc>(magnus::Ruby::get().unwrap().to_symbol("[]"), ("app_proc",))?;
48
49
  let sendfile = params
49
- .funcall::<_, _, bool>(Symbol::new("[]"), ("sendfile",))
50
+ .funcall::<_, _, bool>(magnus::Ruby::get().unwrap().to_symbol("[]"), ("sendfile",))
50
51
  .unwrap_or(true);
51
52
  let nonblocking = params
52
- .funcall::<_, _, bool>(Symbol::new("[]"), ("nonblocking",))
53
+ .funcall::<_, _, bool>(
54
+ magnus::Ruby::get().unwrap().to_symbol("[]"),
55
+ ("nonblocking",),
56
+ )
53
57
  .unwrap_or(false);
54
58
  let base_path_src = params
55
- .funcall::<_, _, String>(Symbol::new("[]"), ("base_path",))
59
+ .funcall::<_, _, String>(magnus::Ruby::get().unwrap().to_symbol("[]"), ("base_path",))
56
60
  .unwrap_or("".to_owned());
57
61
  let script_name = params
58
- .funcall::<_, _, Option<String>>(Symbol::new("[]"), ("script_name",))
62
+ .funcall::<_, _, Option<String>>(
63
+ magnus::Ruby::get().unwrap().to_symbol("[]"),
64
+ ("script_name",),
65
+ )
59
66
  .unwrap_or(None);
60
67
  let base_path = Regex::new(&base_path_src).unwrap();
61
68
 
62
69
  let request_type: RequestType = params
63
- .funcall::<_, _, String>(Symbol::new("[]"), ("request_type",))
70
+ .funcall::<_, _, String>(
71
+ magnus::Ruby::get().unwrap().to_symbol("[]"),
72
+ ("request_type",),
73
+ )
64
74
  .unwrap_or("http".to_string())
65
75
  .parse()
66
76
  .unwrap_or(RequestType::Http);
@@ -27,6 +27,59 @@ use std::{
27
27
  };
28
28
  use tracing::debug;
29
29
 
30
+ /// Compact representation of the client's Accept-Encoding preferences.
31
+ /// Priority order is determined by the bit checks in `pick_encoding`.
32
+ #[derive(Clone, Copy, Debug, Default)]
33
+ struct AcceptEncodingMask(u8);
34
+
35
+ impl AcceptEncodingMask {
36
+ const BR: u8 = 1 << 0;
37
+ const GZIP: u8 = 1 << 1;
38
+ const ZSTD: u8 = 1 << 2;
39
+ const DEFLATE: u8 = 1 << 3;
40
+
41
+ fn from_headers(headers: &[HeaderValue]) -> Self {
42
+ let mut mask = 0u8;
43
+
44
+ for hv in headers {
45
+ let Ok(s) = hv.to_str() else { continue };
46
+
47
+ // We intentionally ignore q-values and treat any mention as "acceptable".
48
+ // This is a fast-path optimization for common benchmark/client headers.
49
+ for part in s.split(',') {
50
+ let token = part.split(';').next().unwrap_or("").trim();
51
+ match token {
52
+ "br" => mask |= Self::BR,
53
+ "gzip" => mask |= Self::GZIP,
54
+ "zstd" => mask |= Self::ZSTD,
55
+ "deflate" => mask |= Self::DEFLATE,
56
+ _ => {}
57
+ }
58
+ }
59
+ }
60
+
61
+ Self(mask)
62
+ }
63
+
64
+ fn pick_encoding(self) -> Option<&'static str> {
65
+ // Prefer stronger/faster compression if available.
66
+ // (Actual availability is checked by the file server.)
67
+ if (self.0 & Self::ZSTD) != 0 {
68
+ return Some("zstd");
69
+ }
70
+ if (self.0 & Self::BR) != 0 {
71
+ return Some("br");
72
+ }
73
+ if (self.0 & Self::GZIP) != 0 {
74
+ return Some("gzip");
75
+ }
76
+ if (self.0 & Self::DEFLATE) != 0 {
77
+ return Some("deflate");
78
+ }
79
+ None
80
+ }
81
+ }
82
+
30
83
  #[derive(Debug, Deserialize)]
31
84
  pub struct StaticAssets {
32
85
  pub root_dir: PathBuf,
@@ -98,7 +151,10 @@ impl MiddlewareLayer for StaticAssets {
98
151
  return Ok(Either::Left(req));
99
152
  }
100
153
 
154
+ // We still populate the context cache for any other middleware that might want it,
155
+ // but we avoid re-parsing Accept-Encoding later by computing a compact mask here.
101
156
  context.set_supported_encoding_set(&req);
157
+
102
158
  let abs_path = req.uri().path();
103
159
  let rel_path = if !self.relative_path {
104
160
  abs_path.trim_start_matches("/")
@@ -119,7 +175,6 @@ impl MiddlewareLayer for StaticAssets {
119
175
  };
120
176
 
121
177
  debug!(target: "middleware::static_assets", "Asset path is {}", rel_path);
122
- // Determine if this is a HEAD request
123
178
  let is_head_request = req.method() == Method::HEAD;
124
179
 
125
180
  // Extract range and if-modified-since headers
@@ -135,6 +190,22 @@ impl MiddlewareLayer for StaticAssets {
135
190
  let encodings: &[HeaderValue] = context
136
191
  .supported_encoding_set()
137
192
  .map_or(&[], |set| set.as_slice());
193
+
194
+ // Compute a fast encoding preference and narrow the encoding list we hand to the server.
195
+ // This avoids repeated per-request string splitting/trim in the static file server.
196
+ let mask = AcceptEncodingMask::from_headers(encodings);
197
+ let preferred = mask.pick_encoding();
198
+
199
+ let narrowed: [HeaderValue; 1];
200
+ let encodings_for_server: &[HeaderValue] = if let Some(token) = preferred {
201
+ // Safe: these are valid header values and the file server only needs to see
202
+ // a minimal representation to pick a cached variant.
203
+ narrowed = [HeaderValue::from_static(token)];
204
+ &narrowed
205
+ } else {
206
+ &[]
207
+ };
208
+
138
209
  let response = file_server
139
210
  .serve(
140
211
  &req,
@@ -143,7 +214,7 @@ impl MiddlewareLayer for StaticAssets {
143
214
  serve_range,
144
215
  if_modified_since,
145
216
  is_head_request,
146
- encodings,
217
+ encodings_for_server,
147
218
  )
148
219
  .await;
149
220
 
@@ -156,40 +227,38 @@ impl MiddlewareLayer for StaticAssets {
156
227
  }
157
228
 
158
229
  fn parse_range_header(headers: &HeaderMap) -> ServeRange {
159
- let range_header = headers.get(RANGE);
160
- if range_header.is_none() {
230
+ let Some(range_header) = headers.get(RANGE) else {
161
231
  return ServeRange::Full;
162
- }
163
- let range_header = range_header.unwrap().to_str().unwrap_or("");
232
+ };
233
+
234
+ let range_header = range_header.to_str().unwrap_or("");
164
235
  let bytes_prefix = "bytes=";
165
236
  if !range_header.starts_with(bytes_prefix) {
166
237
  return ServeRange::Full;
167
238
  }
168
239
 
169
- let range_str = &range_header[bytes_prefix.len()..];
170
-
171
- let range_parts: Vec<&str> = range_str
240
+ // Only consider the first range specifier, ignore multi-range requests.
241
+ let range_str = range_header[bytes_prefix.len()..]
172
242
  .split(',')
173
243
  .next()
174
- .unwrap_or("")
175
- .split('-')
176
- .collect();
177
- if range_parts.len() != 2 {
244
+ .unwrap_or("");
245
+
246
+ let Some((start_str, end_str)) = range_str.split_once('-') else {
178
247
  return ServeRange::Full;
179
- }
248
+ };
180
249
 
181
- let start = if range_parts[0].is_empty() {
182
- range_parts[1].parse::<u64>().unwrap_or(0)
183
- } else if let Ok(start) = range_parts[0].parse::<u64>() {
250
+ let start = if start_str.is_empty() {
251
+ end_str.parse::<u64>().unwrap_or(0)
252
+ } else if let Ok(start) = start_str.parse::<u64>() {
184
253
  start
185
254
  } else {
186
255
  return ServeRange::Full;
187
256
  };
188
257
 
189
- let end = if range_parts[1].is_empty() {
190
- u64::MAX // Use u64::MAX as sentinel for open-ended ranges
191
- } else if let Ok(end) = range_parts[1].parse::<u64>() {
192
- end // No conversion needed, already u64
258
+ let end = if end_str.is_empty() {
259
+ u64::MAX // sentinel for open-ended ranges
260
+ } else if let Ok(end) = end_str.parse::<u64>() {
261
+ end
193
262
  } else {
194
263
  return ServeRange::Full;
195
264
  };