1use core::fmt;
4
5use super::RequestIdPolicy;
6use crate::rate_limit::RateLimit;
7use crate::transport::{
8 MediaType, ResponseBuffer, ResponseContentType, ResponseDecodeWorkspace, ResponseWriterError,
9 RetainedMetadataError, RetainedResponseMetadata, StatusCode, TransportResponse,
10};
11
12#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub enum ResponseBodyPolicy {
15 Required,
17 Optional,
19 Forbidden,
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum ContentTypePolicy {
26 Required(&'static [MediaType<'static>]),
28 Optional(&'static [MediaType<'static>]),
30 Forbidden,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum ResponsePolicyValidationError {
37 MissingSuccessStatus,
39 NonSuccessStatus,
41 DuplicateSuccessStatus,
43 MissingAcceptedMediaType,
45 DuplicateAcceptedMediaType,
47 RequiredBodyHasZeroLimit,
49 ForbiddenBodyHasNonzeroLimit,
51 ForbiddenBodyAllowsContentType,
53 RequiredBodyForbidsContentType,
55}
56
57impl_static_error!(ResponsePolicyValidationError,
58 Self::MissingSuccessStatus => "response policy has no success status",
59 Self::NonSuccessStatus => "response policy contains a non-success status",
60 Self::DuplicateSuccessStatus => "response policy contains duplicate statuses",
61 Self::MissingAcceptedMediaType => "response policy has no accepted media type",
62 Self::DuplicateAcceptedMediaType => "response policy contains duplicate media types",
63 Self::RequiredBodyHasZeroLimit => "required response body has a zero limit",
64 Self::ForbiddenBodyHasNonzeroLimit => "forbidden response body has a nonzero limit",
65 Self::ForbiddenBodyAllowsContentType => "forbidden response body allows a content type",
66 Self::RequiredBodyForbidsContentType => "required response body forbids its content type",
67);
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum ResponsePolicyError {
72 UnexpectedStatus,
74 BodyTooLarge,
76 MissingBody,
78 ForbiddenBody,
80 MissingContentType,
82 InvalidContentType,
84 UnexpectedContentType,
86 ForbiddenContentType,
88 UncommittedResponse,
90 InvalidRequestId,
92}
93
94impl_static_error!(ResponsePolicyError,
95 Self::UnexpectedStatus => "response status is not expected",
96 Self::BodyTooLarge => "response body exceeds the operation limit",
97 Self::MissingBody => "required response body is missing",
98 Self::ForbiddenBody => "response body is forbidden",
99 Self::MissingContentType => "required response content type is missing",
100 Self::InvalidContentType => "response content type is invalid",
101 Self::UnexpectedContentType => "response content type is not accepted",
102 Self::ForbiddenContentType => "response content type is forbidden",
103 Self::UncommittedResponse => "response writer is not committed",
104 Self::InvalidRequestId => "response request identifier is invalid",
105);
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct ResponsePolicy {
110 success_statuses: &'static [StatusCode],
111 content_type: ContentTypePolicy,
112 body: ResponseBodyPolicy,
113 max_body_bytes: usize,
114}
115
116impl ResponsePolicy {
117 pub fn new(
119 success_statuses: &'static [StatusCode],
120 content_type: ContentTypePolicy,
121 body: ResponseBodyPolicy,
122 max_body_bytes: usize,
123 ) -> Result<Self, ResponsePolicyValidationError> {
124 validate_statuses(success_statuses)?;
125 validate_media_types(content_type)?;
126 match (body, content_type, max_body_bytes) {
127 (ResponseBodyPolicy::Required, _, 0) => {
128 return Err(ResponsePolicyValidationError::RequiredBodyHasZeroLimit);
129 }
130 (ResponseBodyPolicy::Forbidden, _, limit) if limit != 0 => {
131 return Err(ResponsePolicyValidationError::ForbiddenBodyHasNonzeroLimit);
132 }
133 (
134 ResponseBodyPolicy::Forbidden,
135 ContentTypePolicy::Required(_) | ContentTypePolicy::Optional(_),
136 _,
137 ) => {
138 return Err(ResponsePolicyValidationError::ForbiddenBodyAllowsContentType);
139 }
140 (ResponseBodyPolicy::Required, ContentTypePolicy::Forbidden, _) => {
141 return Err(ResponsePolicyValidationError::RequiredBodyForbidsContentType);
142 }
143 _ => {}
144 }
145 Ok(Self {
146 success_statuses,
147 content_type,
148 body,
149 max_body_bytes,
150 })
151 }
152
153 #[must_use]
155 pub const fn success_statuses(self) -> &'static [StatusCode] {
156 self.success_statuses
157 }
158 #[must_use]
160 pub const fn content_type_policy(self) -> ContentTypePolicy {
161 self.content_type
162 }
163 #[must_use]
165 pub const fn body_policy(self) -> ResponseBodyPolicy {
166 self.body
167 }
168
169 #[must_use]
171 pub const fn max_body_bytes(self) -> usize {
172 self.max_body_bytes
173 }
174
175 pub fn validate<'buffer>(
177 self,
178 mut writer: ResponseBuffer<'buffer>,
179 request_id_policy: RequestIdPolicy,
180 ) -> Result<CheckedResponseGuard<'buffer>, ResponsePolicyError> {
181 apply_request_id_policy(&mut writer, request_id_policy)?;
182 let snapshot = {
183 let response = writer.response().map_err(map_writer_error)?;
184 self.validate_view(response, request_id_policy)?
185 };
186 Ok(CheckedResponseGuard {
187 writer,
188 snapshot,
189 workspace: ResponseDecodeWorkspace::new(),
190 })
191 }
192
193 fn validate_view(
194 self,
195 response: TransportResponse<'_, '_>,
196 request_id_policy: RequestIdPolicy,
197 ) -> Result<CheckedResponseSnapshot, ResponsePolicyError> {
198 if !self.success_statuses.contains(&response.status()) {
199 return Err(ResponsePolicyError::UnexpectedStatus);
200 }
201 match self.body {
202 ResponseBodyPolicy::Forbidden if !response.body().is_empty() => {
203 return Err(ResponsePolicyError::ForbiddenBody);
204 }
205 _ => {}
206 }
207 if response.body().len() > self.max_body_bytes {
208 return Err(ResponsePolicyError::BodyTooLarge);
209 }
210 if matches!(self.body, ResponseBodyPolicy::Required) && response.body().is_empty() {
211 return Err(ResponsePolicyError::MissingBody);
212 }
213 let content_type = response
214 .content_type()
215 .map_err(|_| ResponsePolicyError::InvalidContentType)?;
216 validate_content_type(self.content_type, content_type)?;
217 Ok(CheckedResponseSnapshot {
218 status: response.status(),
219 body_len: response.body().len(),
220 rate_limit: response.rate_limit(),
221 request_id_policy,
222 })
223 }
224}
225
226pub(crate) fn apply_request_id_policy(
227 writer: &mut ResponseBuffer<'_>,
228 request_id_policy: RequestIdPolicy,
229) -> Result<(), ResponsePolicyError> {
230 writer.response().map_err(map_writer_error)?;
231 writer
232 .apply_request_id_policy(request_id_policy)
233 .map_err(|_| ResponsePolicyError::InvalidRequestId)
234}
235
236#[derive(Clone, Copy)]
238pub struct CheckedResponse<'body> {
239 status: StatusCode,
240 body: &'body [u8],
241 content_type: Option<ResponseContentType<'body>>,
242 rate_limit: Option<RateLimit>,
243 request_id: Option<&'body [u8]>,
244 request_id_policy: RequestIdPolicy,
245}
246
247impl<'body> CheckedResponse<'body> {
248 #[must_use]
250 pub const fn status(&self) -> StatusCode {
251 self.status
252 }
253
254 #[must_use]
256 pub const fn body(&self) -> &[u8] {
257 self.body
258 }
259
260 #[must_use]
262 pub const fn content_type(&self) -> Option<ResponseContentType<'body>> {
263 self.content_type
264 }
265
266 #[must_use]
268 pub const fn rate_limit(&self) -> Option<RateLimit> {
269 self.rate_limit
270 }
271
272 #[must_use]
274 pub const fn request_id_policy(&self) -> RequestIdPolicy {
275 self.request_id_policy
276 }
277
278 pub fn with_request_id<R>(&self, inspect: impl FnOnce(Option<&[u8]>) -> R) -> R {
280 inspect(self.request_id)
281 }
282}
283
284#[derive(Clone, Copy)]
285struct CheckedResponseSnapshot {
286 status: StatusCode,
287 body_len: usize,
288 rate_limit: Option<RateLimit>,
289 request_id_policy: RequestIdPolicy,
290}
291
292pub struct CheckedResponseGuard<'buffer> {
297 writer: ResponseBuffer<'buffer>,
298 snapshot: CheckedResponseSnapshot,
299 workspace: ResponseDecodeWorkspace,
300}
301
302impl CheckedResponseGuard<'_> {
303 #[must_use]
305 pub const fn status(&self) -> StatusCode {
306 self.snapshot.status
307 }
308
309 #[must_use]
311 pub fn content_type(&self) -> Option<ResponseContentType<'_>> {
312 self.writer
313 .response()
314 .ok()
315 .and_then(|response| response.content_type().ok().flatten())
316 }
317
318 #[must_use]
320 pub const fn rate_limit(&self) -> Option<RateLimit> {
321 self.snapshot.rate_limit
322 }
323
324 pub fn with_borrowed<R>(
335 &self,
336 inspect: impl for<'response> FnOnce(CheckedResponse<'response>) -> R,
337 ) -> R {
338 inspect(self.checked_response())
339 }
340
341 pub fn decode_owned<R, E>(
343 self,
344 decode: impl for<'response> FnOnce(CheckedResponse<'response>) -> Result<R, E>,
345 ) -> Result<R, E> {
346 self.decode_owned_with_workspace(|response, _workspace| decode(response))
347 }
348
349 pub fn decode_owned_with_workspace<R, E>(
351 mut self,
352 decode: impl for<'response> FnOnce(
353 CheckedResponse<'response>,
354 &mut ResponseDecodeWorkspace,
355 ) -> Result<R, E>,
356 ) -> Result<R, E> {
357 let result = {
358 let Self {
359 writer,
360 snapshot,
361 workspace,
362 } = &mut self;
363 let response = checked_response(writer, *snapshot);
364 decode(response, workspace)
365 };
366 drop(self);
367 result
368 }
369
370 pub fn retain_metadata_into<'destination>(
372 &mut self,
373 destination: &'destination mut [u8],
374 request_id_limit: usize,
375 ) -> Result<RetainedResponseMetadata<'destination>, RetainedMetadataError> {
376 if self.snapshot.request_id_policy != RequestIdPolicy::Retain {
377 return Err(RetainedMetadataError::RetentionForbidden);
378 }
379 self.writer.retain_request_id(destination, request_id_limit)
380 }
381
382 fn checked_response(&self) -> CheckedResponse<'_> {
383 checked_response(&self.writer, self.snapshot)
384 }
385}
386
387impl fmt::Debug for CheckedResponseGuard<'_> {
388 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
389 formatter
390 .debug_struct("CheckedResponseGuard")
391 .field("status", &self.status())
392 .field("body_len", &self.snapshot.body_len)
393 .field("body", &"[redacted]")
394 .field("content_type", &self.content_type())
395 .field("rate_limit", &self.rate_limit())
396 .field("request_id", &"[redacted]")
397 .finish()
398 }
399}
400
401impl fmt::Debug for CheckedResponse<'_> {
402 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
403 formatter
404 .debug_struct("CheckedResponse")
405 .field("status", &self.status())
406 .field("body_len", &self.body().len())
407 .field("body", &"[redacted]")
408 .field("content_type", &self.content_type())
409 .field("rate_limit", &self.rate_limit())
410 .field("request_id", &"[redacted]")
411 .finish()
412 }
413}
414
415fn checked_response<'response>(
416 writer: &'response ResponseBuffer<'_>,
417 snapshot: CheckedResponseSnapshot,
418) -> CheckedResponse<'response> {
419 CheckedResponse {
420 status: snapshot.status,
421 body: writer.initialized_body(snapshot.body_len),
422 content_type: writer
423 .response()
424 .ok()
425 .and_then(|response| response.content_type().ok().flatten()),
426 rate_limit: snapshot.rate_limit,
427 request_id: writer.request_id(),
428 request_id_policy: snapshot.request_id_policy,
429 }
430}
431
432fn validate_statuses(statuses: &[StatusCode]) -> Result<(), ResponsePolicyValidationError> {
433 if statuses.is_empty() {
434 return Err(ResponsePolicyValidationError::MissingSuccessStatus);
435 }
436 for (index, status) in statuses.iter().enumerate() {
437 if !status.is_success() {
438 return Err(ResponsePolicyValidationError::NonSuccessStatus);
439 }
440 if statuses
441 .get(..index)
442 .is_some_and(|seen| seen.contains(status))
443 {
444 return Err(ResponsePolicyValidationError::DuplicateSuccessStatus);
445 }
446 }
447 Ok(())
448}
449
450fn validate_media_types(policy: ContentTypePolicy) -> Result<(), ResponsePolicyValidationError> {
451 let media_types = match policy {
452 ContentTypePolicy::Required(values) | ContentTypePolicy::Optional(values) => values,
453 ContentTypePolicy::Forbidden => return Ok(()),
454 };
455 if media_types.is_empty() {
456 return Err(ResponsePolicyValidationError::MissingAcceptedMediaType);
457 }
458 for (index, media_type) in media_types.iter().enumerate() {
459 if media_types.get(..index).is_some_and(|seen| {
460 seen.iter()
461 .any(|candidate| candidate.as_str().eq_ignore_ascii_case(media_type.as_str()))
462 }) {
463 return Err(ResponsePolicyValidationError::DuplicateAcceptedMediaType);
464 }
465 }
466 Ok(())
467}
468
469fn validate_content_type(
470 policy: ContentTypePolicy,
471 actual: Option<ResponseContentType<'_>>,
472) -> Result<(), ResponsePolicyError> {
473 match (policy, actual) {
474 (ContentTypePolicy::Required(_), None) => Err(ResponsePolicyError::MissingContentType),
475 (ContentTypePolicy::Forbidden, Some(_)) => Err(ResponsePolicyError::ForbiddenContentType),
476 (ContentTypePolicy::Forbidden | ContentTypePolicy::Optional(_), None) => Ok(()),
477 (
478 ContentTypePolicy::Required(accepted) | ContentTypePolicy::Optional(accepted),
479 Some(actual),
480 ) => {
481 if accepted
482 .iter()
483 .any(|media_type| actual.matches(*media_type))
484 {
485 Ok(())
486 } else {
487 Err(ResponsePolicyError::UnexpectedContentType)
488 }
489 }
490 }
491}
492const fn map_writer_error(error: ResponseWriterError) -> ResponsePolicyError {
493 match error {
494 ResponseWriterError::NotCommitted
495 | ResponseWriterError::AlreadyCommitted
496 | ResponseWriterError::InitializedLengthTooLarge => {
497 ResponsePolicyError::UncommittedResponse
498 }
499 }
500}