Skip to content

Commit 5f27a55

Browse files
committed
merge main
2 parents 76e00b2 + 81ec815 commit 5f27a55

21 files changed

+526
-117
lines changed

.github/workflows/validate-openapi-spec.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
1515
- uses: actions/setup-node@v6
1616
with:
17-
node-version: '22'
17+
node-version: '24'
1818
- name: Install our tools
1919
shell: bash
2020
run: |

CHANGELOG.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
https://github.com/oxidecomputer/dropshot/compare/v0.16.4\...HEAD[Full list of commits]
1717

18+
* https://github.com/oxidecomputer/dropshot/pull/1475[#1475] Added `ClientSpecifiesVersionInHeader::on_missing` to provide a default version when the header is missing, intended for use when you're not in control of all clients
19+
1820
== 0.16.4 (released 2025-09-04)
1921

2022
https://github.com/oxidecomputer/dropshot/compare/v0.16.3\...v0.16.4[Full list of commits]

Cargo.lock

Lines changed: 18 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dropshot/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ version = "^0.16.4"
5959
path = "../dropshot_endpoint"
6060

6161
[dependencies.hyper]
62-
version = "1.7.0"
62+
version = "1.8.1"
6363
features = [ "full" ]
6464

6565
[dependencies.hyper-util]
66-
version = "0.1.17"
66+
version = "0.1.18"
6767
features = [ "full" ]
6868

6969
[dependencies.openapiv3]
@@ -110,7 +110,7 @@ libc = "0.2.177"
110110
mime_guess = "2.0.5"
111111
subprocess = "0.2.9"
112112
tempfile = "3.23"
113-
trybuild = "1.0.112"
113+
trybuild = "1.0.114"
114114
# Used by the https examples and tests
115115
pem = "3.0"
116116
rcgen = "0.14.5"
@@ -124,7 +124,7 @@ version = "0.12.24"
124124
features = [ "json", "rustls-tls" ]
125125

126126
[dev-dependencies.rustls-pki-types]
127-
version = "1.12.0"
127+
version = "1.13.0"
128128
# Needed for CertificateDer::into_owned
129129
features = ["alloc"]
130130

dropshot/src/api_description.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -301,24 +301,19 @@ pub enum ApiEndpointParameterMetadata {
301301
Body(ApiEndpointBodyContentType),
302302
}
303303

304-
#[derive(Debug, Clone)]
304+
#[derive(Debug, Clone, Default)]
305305
pub enum ApiEndpointBodyContentType {
306306
/// application/octet-stream
307307
Bytes,
308308
/// application/json
309+
#[default]
309310
Json,
310311
/// application/x-www-form-urlencoded
311312
UrlEncoded,
312313
/// multipart/form-data
313314
MultipartFormData,
314315
}
315316

316-
impl Default for ApiEndpointBodyContentType {
317-
fn default() -> Self {
318-
Self::Json
319-
}
320-
}
321-
322317
impl ApiEndpointBodyContentType {
323318
pub fn mime_type(&self) -> &str {
324319
match self {

dropshot/src/versioning.rs

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ pub trait DynamicVersionPolicy: std::fmt::Debug + Send + Sync {
114114
) -> Result<Version, HttpError>;
115115
}
116116

117-
/// Implementation of `DynamicVersionPolicy` where the client must specify a
117+
/// Implementation of `DynamicVersionPolicy` where the client specifies a
118118
/// specific semver in a specific header and we always use whatever they
119-
/// requested
119+
/// requested.
120120
///
121121
/// An incoming request will be rejected with a 400-level error if:
122122
///
@@ -125,17 +125,22 @@ pub trait DynamicVersionPolicy: std::fmt::Debug + Send + Sync {
125125
/// [`ClientSpecifiesVersionInHeader::new()`], which implies that the client
126126
/// is trying to use a newer version of the API than this server supports.
127127
///
128+
/// By default, incoming requests will also be rejected with a 400-level error
129+
/// if the header is missing. To override this behavior, supply a default
130+
/// version via [`Self::on_missing`].
131+
///
128132
/// If you need anything more flexible (e.g., validating the provided version
129133
/// against a fixed set of supported versions), you'll want to impl
130134
/// `DynamicVersionPolicy` yourself.
131135
#[derive(Debug)]
132136
pub struct ClientSpecifiesVersionInHeader {
133137
name: HeaderName,
134138
max_version: Version,
139+
on_missing: Option<Version>,
135140
}
136141

137142
impl ClientSpecifiesVersionInHeader {
138-
/// Make a new `ClientSpecifiesVersionInHeader` policy
143+
/// Make a new `ClientSpecifiesVersionInHeader` policy.
139144
///
140145
/// Arguments:
141146
///
@@ -148,7 +153,25 @@ impl ClientSpecifiesVersionInHeader {
148153
name: HeaderName,
149154
max_version: Version,
150155
) -> ClientSpecifiesVersionInHeader {
151-
ClientSpecifiesVersionInHeader { name, max_version }
156+
ClientSpecifiesVersionInHeader { name, max_version, on_missing: None }
157+
}
158+
159+
/// If the header is missing, use the provided version instead.
160+
///
161+
/// By default, the policy will reject requests with a missing header. Call
162+
/// this function to use the provided version instead.
163+
///
164+
/// Typically, the provided version should either be a fixed supported
165+
/// version (for backwards compatibility with older clients), or the newest
166+
/// supported version (in case clients are generally kept up-to-date but not
167+
/// all clients send the header).
168+
///
169+
/// Using this function is not recommended if you control all clients—in
170+
/// that case, arrange for clients to send the header instead. In
171+
/// particular, **at Oxide, do not use this function for internal APIs.**
172+
pub fn on_missing(mut self, version: Version) -> Self {
173+
self.on_missing = Some(version);
174+
self
152175
}
153176
}
154177

@@ -159,13 +182,25 @@ impl DynamicVersionPolicy for ClientSpecifiesVersionInHeader {
159182
_log: &Logger,
160183
) -> Result<Version, HttpError> {
161184
let v = parse_header(request.headers(), &self.name)?;
162-
if v <= self.max_version {
163-
Ok(v)
164-
} else {
165-
Err(HttpError::for_bad_request(
185+
match (v, &self.on_missing) {
186+
(Some(v), _) => {
187+
if v <= self.max_version {
188+
Ok(v)
189+
} else {
190+
Err(HttpError::for_bad_request(
191+
None,
192+
format!(
193+
"server does not support this API version: {}",
194+
v
195+
),
196+
))
197+
}
198+
}
199+
(None, Some(on_missing)) => Ok(on_missing.clone()),
200+
(None, None) => Err(HttpError::for_bad_request(
166201
None,
167-
format!("server does not support this API version: {}", v),
168-
))
202+
format!("missing expected header {:?}", self.name),
203+
)),
169204
}
170205
}
171206
}
@@ -175,17 +210,12 @@ impl DynamicVersionPolicy for ClientSpecifiesVersionInHeader {
175210
fn parse_header<T>(
176211
headers: &http::HeaderMap,
177212
header_name: &HeaderName,
178-
) -> Result<T, HttpError>
213+
) -> Result<Option<T>, HttpError>
179214
where
180215
T: FromStr,
181216
<T as FromStr>::Err: std::fmt::Display,
182217
{
183-
let v_value = headers.get(header_name).ok_or_else(|| {
184-
HttpError::for_bad_request(
185-
None,
186-
format!("missing expected header {:?}", header_name),
187-
)
188-
})?;
218+
let Some(v_value) = headers.get(header_name) else { return Ok(None) };
189219

190220
let v_str = v_value.to_str().map_err(|_| {
191221
HttpError::for_bad_request(
@@ -197,10 +227,12 @@ where
197227
)
198228
})?;
199229

200-
v_str.parse::<T>().map_err(|e| {
230+
let v = v_str.parse::<T>().map_err(|e| {
201231
HttpError::for_bad_request(
202232
None,
203233
format!("bad value for header {:?}: {}: {}", header_name, e, v_str),
204234
)
205-
})
235+
})?;
236+
237+
Ok(Some(v))
206238
}

0 commit comments

Comments
 (0)