Skip to content

Commit 3d371c4

Browse files
authored
accept body parameters of the form application/subtype+json (#1413)
1 parent a65d153 commit 3d371c4

File tree

2 files changed

+71
-11
lines changed

2 files changed

+71
-11
lines changed

dropshot/src/api_description.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,31 @@ impl ApiEndpointBodyContentType {
335335
CONTENT_TYPE_JSON => Ok(Self::Json),
336336
CONTENT_TYPE_URL_ENCODED => Ok(Self::UrlEncoded),
337337
CONTENT_TYPE_MULTIPART_FORM_DATA => Ok(Self::MultipartFormData),
338-
_ => Err(mime_type.to_string()),
338+
_ => match mime_split(mime_type) {
339+
// We may see content-type that is of the form
340+
// application/XXX+json which means "XXX protocol serialized as
341+
// JSON". A more pedantic implementation might involve a server
342+
// (or subset of its API) indicating that it expects (and
343+
// produces) bodies in a particular format, but for now it
344+
// suffices to treat input bodies of this form as equivalent to
345+
// application/json.
346+
Some(("application", _, Some("json"))) => Ok(Self::Json),
347+
_ => Err(mime_type.to_string()),
348+
},
339349
}
340350
}
341351
}
342352

353+
/// Split the mime type in to the type, subtype, and optional suffix
354+
/// components.
355+
fn mime_split(mime_type: &str) -> Option<(&str, &str, Option<&str>)> {
356+
let (type_, rest) = mime_type.split_once('/')?;
357+
let mut sub_parts = rest.splitn(2, '+');
358+
let subtype = sub_parts.next()?;
359+
let suffix = sub_parts.next();
360+
Some((type_, subtype, suffix))
361+
}
362+
343363
#[derive(Debug)]
344364
pub struct ApiEndpointHeader {
345365
pub name: String,

dropshot/src/extractor/body.rs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,16 @@ impl ExclusiveExtractor for MultipartBody {
121121

122122
/// Given an HTTP request, attempt to read the body, parse it according
123123
/// to the content type, and deserialize it to an instance of `BodyType`.
124-
async fn http_request_load_body<Context: ServerContext, BodyType>(
125-
rqctx: &RequestContext<Context>,
124+
async fn http_request_load_body<BodyType>(
126125
request: hyper::Request<crate::Body>,
126+
request_body_max_bytes: usize,
127+
expected_body_content_type: &ApiEndpointBodyContentType,
127128
) -> Result<TypedBody<BodyType>, HttpError>
128129
where
129130
BodyType: JsonSchema + DeserializeOwned + Send + Sync,
130131
{
131132
let (parts, body) = request.into_parts();
132-
let body = StreamingBody::new(body, rqctx.request_body_max_bytes())
133+
let body = StreamingBody::new(body, request_body_max_bytes)
133134
.into_bytes_mut()
134135
.await?;
135136

@@ -150,14 +151,19 @@ where
150151
.unwrap_or(Ok(CONTENT_TYPE_JSON))?;
151152
let end = content_type.find(';').unwrap_or_else(|| content_type.len());
152153
let mime_type = content_type[..end].trim_end().to_lowercase();
153-
let body_content_type =
154-
ApiEndpointBodyContentType::from_mime_type(&mime_type)
155-
.map_err(|e| HttpError::for_bad_request(None, e))?;
156-
let expected_content_type = rqctx.endpoint.body_content_type.clone();
154+
let body_content_type = ApiEndpointBodyContentType::from_mime_type(
155+
&mime_type,
156+
)
157+
.map_err(|e| {
158+
HttpError::for_bad_request(
159+
None,
160+
format!("unsupported content-type: {}", e),
161+
)
162+
})?;
157163

158164
use ApiEndpointBodyContentType::*;
159165

160-
let content = match (expected_content_type, body_content_type) {
166+
let content = match (expected_body_content_type, body_content_type) {
161167
(Json, Json) => {
162168
let jd = &mut serde_json::Deserializer::from_slice(&body);
163169
serde_path_to_error::deserialize(jd).map_err(|e| {
@@ -186,7 +192,7 @@ where
186192
expected.mime_type(),
187193
requested.mime_type()
188194
),
189-
))
195+
));
190196
}
191197
};
192198
Ok(TypedBody { inner: content })
@@ -207,7 +213,12 @@ where
207213
rqctx: &RequestContext<Context>,
208214
request: hyper::Request<crate::Body>,
209215
) -> Result<TypedBody<BodyType>, HttpError> {
210-
http_request_load_body(rqctx, request).await
216+
http_request_load_body(
217+
request,
218+
rqctx.request_body_max_bytes(),
219+
&rqctx.endpoint.body_content_type,
220+
)
221+
.await
211222
}
212223

213224
fn metadata(content_type: ApiEndpointBodyContentType) -> ExtractorMetadata {
@@ -457,3 +468,32 @@ fn untyped_metadata() -> ExtractorMetadata {
457468
extension_mode: ExtensionMode::None,
458469
}
459470
}
471+
472+
#[cfg(test)]
473+
mod tests {
474+
use schemars::JsonSchema;
475+
use serde::Deserialize;
476+
477+
use crate::extractor::body::http_request_load_body;
478+
479+
#[tokio::test]
480+
async fn test_content_plus_json() {
481+
#[derive(Deserialize, JsonSchema)]
482+
struct TheRealScimShady {}
483+
484+
let body = "{}";
485+
let request = hyper::Request::builder()
486+
.header(http::header::CONTENT_TYPE, "application/scim+json")
487+
.body(crate::Body::with_content(body))
488+
.unwrap();
489+
490+
let r = http_request_load_body::<TheRealScimShady>(
491+
request,
492+
9000,
493+
&crate::ApiEndpointBodyContentType::Json,
494+
)
495+
.await;
496+
497+
assert!(r.is_ok())
498+
}
499+
}

0 commit comments

Comments
 (0)