Skip to content

Commit d5874eb

Browse files
authored
[5/n] [dropshot] add support for per-endpoint size limits (#1171)
For some endpoints like uploading update and support bundles, we would like to have a larger request limit. Add support for that. This is the other half of RFD 353, after #617. (A previous attempt was #618, which I've abandoned.) The PR consists of: * The implementation. * A small tweak to the API generation code for better span attribution. * Tests.
1 parent 05af62e commit d5874eb

19 files changed

+559
-38
lines changed

CHANGELOG.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ Defining the old config option will produce an error, guiding you to perform the
2626
** `rqctx.path_variables` is now `rqctx.endpoint.variables`.
2727
** `rqctx.body_content_type` is now `rqctx.endpoint.body_content_type`.
2828

29+
=== Other notable changes
30+
31+
* Dropshot now supports per-endpoint size limits, via the `request_body_max_bytes` parameter to `#[endpoint]`. For example, to set a limit of 1 MiB on an endpoint:
32+
+
33+
```rust
34+
#[endpoint {
35+
method = POST,
36+
path = "/upload-bundle",
37+
request_body_max_bytes = 1 * 1024 * 1024,
38+
}]
39+
async fn upload_bundle(
40+
rqctx: RequestContext<MyContext>, // or RequestContext<Self::Context> with API traits
41+
body: UntypedBody,
42+
) -> /* ... */ {
43+
// ...
44+
}
45+
```
46+
+
47+
If not specified, the limit defaults to the server configuration's `default_request_body_max_bytes`.
48+
2949
== 0.13.0 (released 2024-11-13)
3050

3151
https://github.com/oxidecomputer/dropshot/compare/v0.12.0\...v0.13.0[Full list of commits]

README.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ include:
5050
|No
5151
|Specifies the maximum number of bytes allowed in a request body. Larger requests will receive a 400 error. Defaults to 1024.
5252

53+
Can be overridden per-endpoint via the `request_body_max_bytes` parameter to `#[endpoint { ... }]`.
54+
5355
|`tls.type`
5456
|`"AsFile"`
5557
|No

dropshot/src/api_description.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ pub struct ApiEndpoint<Context: ServerContext> {
5252
pub path: String,
5353
pub parameters: Vec<ApiEndpointParameter>,
5454
pub body_content_type: ApiEndpointBodyContentType,
55+
/// An override for the maximum allowed size of the request body.
56+
///
57+
/// `None` means that the server default is used.
58+
pub request_body_max_bytes: Option<usize>,
5559
pub response: ApiEndpointResponse,
5660
pub summary: Option<String>,
5761
pub description: Option<String>,
@@ -88,6 +92,7 @@ impl<'a, Context: ServerContext> ApiEndpoint<Context> {
8892
path: path.to_string(),
8993
parameters: func_parameters.parameters,
9094
body_content_type,
95+
request_body_max_bytes: None,
9196
response,
9297
summary: None,
9398
description: None,
@@ -109,6 +114,11 @@ impl<'a, Context: ServerContext> ApiEndpoint<Context> {
109114
self
110115
}
111116

117+
pub fn request_body_max_bytes(mut self, max_bytes: usize) -> Self {
118+
self.request_body_max_bytes = Some(max_bytes);
119+
self
120+
}
121+
112122
pub fn tag<T: ToString>(mut self, tag: T) -> Self {
113123
self.tags.push(tag.to_string());
114124
self
@@ -188,6 +198,7 @@ impl<'a> ApiEndpoint<StubContext> {
188198
path: path.to_string(),
189199
parameters: func_parameters.parameters,
190200
body_content_type,
201+
request_body_max_bytes: None,
191202
response,
192203
summary: None,
193204
description: None,

dropshot/src/handler.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,14 @@ impl<Context: ServerContext> RequestContext<Context> {
174174
}
175175

176176
/// Returns the maximum request body size.
177+
///
178+
/// This is typically the same as
179+
/// `self.server.config.request_body_max_bytes`, but can be overridden on a
180+
/// per-endpoint basis.
177181
pub fn request_body_max_bytes(&self) -> usize {
178-
self.server.config.default_request_body_max_bytes
182+
self.endpoint
183+
.request_body_max_bytes
184+
.unwrap_or(self.server.config.default_request_body_max_bytes)
179185
}
180186

181187
/// Returns the appropriate count of items to return for a paginated request
@@ -218,6 +224,9 @@ pub struct RequestEndpointMetadata {
218224

219225
/// The expected request body MIME type.
220226
pub body_content_type: ApiEndpointBodyContentType,
227+
228+
/// The maximum number of bytes allowed in the request body, if overridden.
229+
pub request_body_max_bytes: Option<usize>,
221230
}
222231

223232
/// Helper trait for extracting the underlying Context type from the

dropshot/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@
707707
//!
708708
//! ```text
709709
//! // introduced in 1.0.0, present in all subsequent versions
710-
//! versions = "1.0.0"..
710+
//! versions = "1.0.0"..
711711
//!
712712
//! // removed in 2.0.0, present in all previous versions
713713
//! // (not present in 2.0.0 itself)

dropshot/src/router.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ impl<Context: ServerContext> HttpRouter<Context> {
520520
operation_id: handler.operation_id.clone(),
521521
variables,
522522
body_content_type: handler.body_content_type.clone(),
523+
request_body_max_bytes: handler.request_body_max_bytes,
523524
},
524525
});
525526
}
@@ -874,6 +875,7 @@ mod test {
874875
path: path.to_string(),
875876
parameters: vec![],
876877
body_content_type: ApiEndpointBodyContentType::default(),
878+
request_body_max_bytes: None,
877879
response: ApiEndpointResponse::default(),
878880
summary: None,
879881
description: None,

dropshot/src/websocket.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ mod tests {
403403
operation_id: "".to_string(),
404404
variables: Default::default(),
405405
body_content_type: Default::default(),
406+
request_body_max_bytes: None,
406407
},
407408
request_id: "".to_string(),
408409
log: log.clone(),

dropshot/tests/fail/bad_channel28.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2024 Oxide Computer Company
2+
3+
#![allow(unused_imports)]
4+
5+
use dropshot::channel;
6+
use dropshot::RequestContext;
7+
use dropshot::WebsocketUpgrade;
8+
9+
// Test: request_body_max_bytes specified for channel (this parameter is only
10+
// accepted for endpoints, not channels)
11+
12+
#[channel {
13+
protocol = WEBSOCKETS,
14+
path = "/test",
15+
request_body_max_bytes = 1024,
16+
}]
17+
async fn bad_channel(
18+
_rqctx: RequestContext<()>,
19+
_upgraded: WebsocketUpgrade,
20+
) -> dropshot::WebsocketChannelResult {
21+
Ok(())
22+
}
23+
24+
fn main() {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error: extraneous member `request_body_max_bytes`
2+
--> tests/fail/bad_channel28.rs:15:5
3+
|
4+
15 | request_body_max_bytes = 1024,
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

dropshot/tests/fail/bad_endpoint28.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2024 Oxide Computer Company
2+
3+
#![allow(unused_imports)]
4+
5+
use dropshot::endpoint;
6+
use dropshot::HttpError;
7+
use dropshot::HttpResponseUpdatedNoContent;
8+
use dropshot::RequestContext;
9+
10+
// Test: incorrect type for request_body_max_bytes.
11+
12+
#[endpoint {
13+
method = GET,
14+
path = "/test",
15+
request_body_max_bytes = "not_a_number"
16+
}]
17+
async fn bad_endpoint(
18+
_rqctx: RequestContext<()>,
19+
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
20+
Ok(HttpResponseUpdatedNoContent())
21+
}
22+
23+
fn main() {}

0 commit comments

Comments
 (0)