Skip to main content

wip_http/
codec.rs

1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3
4use http::header::{ACCEPT, CONTENT_ENCODING, CONTENT_TYPE};
5use http::{HeaderMap, Method, Request, Response, StatusCode};
6use wip_protocol::{
7    CallOperationRequest, CallOperationResponse, FetchInterfaceRequest, FetchInterfaceResponse,
8    INTERFACE_FORMAT_V1, InterfaceDescriptor, InterfaceTarget, ObserveRequest, ObserveResponse,
9    ProtocolError, ProtocolInteraction, Target,
10};
11
12use crate::response::{self, DecodedResponse};
13use crate::{
14    BodyKind, ClientResponseError, CodecError, Endpoint, InvalidResponseKind, JSON_CONTENT_TYPE,
15    Limits, Route, json,
16};
17
18fn request(
19    endpoint: &Endpoint,
20    route: Route,
21    body: Vec<u8>,
22) -> Result<Request<Vec<u8>>, http::Error> {
23    Request::builder()
24        .method(Method::POST)
25        .uri(endpoint.route(route).as_str())
26        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
27        .header(ACCEPT, "application/json")
28        .body(body)
29}
30
31fn response(status: StatusCode, body: Vec<u8>) -> Result<Response<Vec<u8>>, http::Error> {
32    Response::builder()
33        .status(status)
34        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
35        .body(body)
36}
37
38fn require_response_content_type<B>(response: &Response<B>) -> Result<(), ClientResponseError> {
39    if !has_supported_content_coding(response.headers()) {
40        return Err(response::unsupported_content_coding(response.status()));
41    }
42    let valid = response
43        .headers()
44        .get(CONTENT_TYPE)
45        .and_then(|value| value.to_str().ok())
46        .is_some_and(is_json_content_type);
47    if valid {
48        Ok(())
49    } else if response.status().is_success() {
50        Err(ClientResponseError::invalid(
51            InvalidResponseKind::InvalidSuccessBody,
52            "missing or non-UTF-8 application/json Content-Type",
53        ))
54    } else {
55        Err(response::transport_failure(response.status()))
56    }
57}
58
59fn is_utf8_charset(value: &str) -> bool {
60    let value = value.trim();
61    value.eq_ignore_ascii_case("utf-8")
62        || value
63            .strip_prefix('"')
64            .and_then(|value| value.strip_suffix('"'))
65            .is_some_and(|value| value.eq_ignore_ascii_case("utf-8"))
66}
67
68/// Returns whether a Content-Type value denotes UTF-8 `application/json`.
69///
70/// The charset parameter is optional because JSON exchanged by this binding is
71/// always UTF-8; when present, its value must be `utf-8`. Other parameters are
72/// rejected so intermediaries cannot silently select another representation.
73#[must_use]
74pub fn is_json_content_type(value: &str) -> bool {
75    let mut parts = value.split(';');
76    if !parts
77        .next()
78        .is_some_and(|media| media.trim().eq_ignore_ascii_case("application/json"))
79    {
80        return false;
81    }
82    let mut saw_charset = false;
83    for parameter in parts {
84        let Some((name, value)) = parameter.trim().split_once('=') else {
85            return false;
86        };
87        if !name.trim().eq_ignore_ascii_case("charset") || !is_utf8_charset(value) || saw_charset {
88            return false;
89        }
90        saw_charset = true;
91    }
92    true
93}
94
95fn accepts_json(headers: &HeaderMap) -> bool {
96    let mut present = false;
97    let mut best_match: Option<((u8, u8), f32)> = None;
98    for value in headers.get_all(ACCEPT) {
99        present = true;
100        let Ok(value) = value.to_str() else {
101            continue;
102        };
103        for range in value.split(',') {
104            let mut parts = range.split(';');
105            let media_range = parts.next().unwrap_or_default().trim();
106            let mut quality = 1.0_f32;
107            let mut saw_quality = false;
108            let mut saw_charset = false;
109            let mut media_parameter_count = 0;
110            let mut valid = true;
111            for parameter in parts {
112                let parameter = parameter.trim();
113                let (name, value) = parameter
114                    .split_once('=')
115                    .map_or((parameter, None), |(name, value)| {
116                        (name.trim(), Some(value))
117                    });
118                if name.is_empty() {
119                    valid = false;
120                    break;
121                }
122                if saw_quality {
123                    // Parameters after q are Accept extensions and do not
124                    // constrain the selected representation.
125                    continue;
126                }
127                if name.eq_ignore_ascii_case("q") {
128                    let Some(value) = value else {
129                        valid = false;
130                        break;
131                    };
132                    saw_quality = true;
133                    match value.trim().parse::<f32>() {
134                        Ok(value) if (0.0..=1.0).contains(&value) => quality = value,
135                        _ => {
136                            valid = false;
137                            break;
138                        }
139                    }
140                } else {
141                    let Some(value) = value else {
142                        valid = false;
143                        break;
144                    };
145                    if !name.eq_ignore_ascii_case("charset")
146                        || !is_utf8_charset(value)
147                        || saw_charset
148                    {
149                        valid = false;
150                        break;
151                    }
152                    saw_charset = true;
153                    media_parameter_count += 1;
154                }
155            }
156            let media_specificity = if media_range.eq_ignore_ascii_case("application/json") {
157                Some(2)
158            } else if media_range.eq_ignore_ascii_case("application/*") {
159                Some(1)
160            } else if media_range == "*/*" {
161                Some(0)
162            } else {
163                None
164            };
165            if valid && let Some(media_specificity) = media_specificity {
166                let specificity = (media_specificity, media_parameter_count);
167                match best_match {
168                    Some((best_specificity, _)) if best_specificity > specificity => {}
169                    Some((best_specificity, best_quality)) if best_specificity == specificity => {
170                        best_match = Some((specificity, best_quality.max(quality)));
171                    }
172                    _ => best_match = Some((specificity, quality)),
173                }
174            }
175        }
176    }
177    if present {
178        best_match.is_some_and(|(_, quality)| quality > 0.0)
179    } else {
180        true
181    }
182}
183
184/// A failure while validating inbound HTTP request metadata.
185#[derive(Debug)]
186pub enum RequestMetadataError {
187    /// The request URI does not exactly identify the selected standard route.
188    NotFound,
189    /// A known standard route was requested with a method other than `POST`.
190    MethodNotAllowed,
191    /// The request body does not use UTF-8 `application/json` or identity coding.
192    UnsupportedMediaType,
193    /// The request's `Accept` metadata excludes `application/json`.
194    NotAcceptable,
195}
196
197impl RequestMetadataError {
198    /// Returns the HTTP status for this metadata failure.
199    #[must_use]
200    pub const fn status(&self) -> StatusCode {
201        match self {
202            Self::NotFound => StatusCode::NOT_FOUND,
203            Self::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
204            Self::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
205            Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE,
206        }
207    }
208
209    /// Returns the value for an `Allow` response header, when required.
210    #[must_use]
211    pub const fn allow(&self) -> Option<&'static str> {
212        match self {
213            Self::MethodNotAllowed => Some("POST"),
214            Self::NotFound | Self::UnsupportedMediaType | Self::NotAcceptable => None,
215        }
216    }
217}
218
219impl Display for RequestMetadataError {
220    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
221        match self {
222            Self::NotFound => formatter.write_str("request URI is not a standard WIP route"),
223            Self::MethodNotAllowed => formatter.write_str("standard WIP routes require POST"),
224            Self::UnsupportedMediaType => {
225                formatter.write_str("expected identity-coded UTF-8 application/json Content-Type")
226            }
227            Self::NotAcceptable => formatter.write_str("request does not accept application/json"),
228        }
229    }
230}
231
232impl Error for RequestMetadataError {}
233
234pub(crate) fn has_supported_content_coding(headers: &HeaderMap) -> bool {
235    headers.get_all(CONTENT_ENCODING).iter().all(|value| {
236        value.to_str().is_ok_and(|value| {
237            let mut codings = value.split(',').map(str::trim);
238            let first = codings.next();
239            first.is_some_and(|coding| coding.eq_ignore_ascii_case("identity"))
240                && codings.all(|coding| coding.eq_ignore_ascii_case("identity"))
241        })
242    })
243}
244
245/// Validates mandatory HTTP metadata for an inbound WIP request.
246///
247/// Both absolute-form and origin-form request URIs are accepted, but the path
248/// must exactly equal the selected endpoint route and a query is forbidden.
249/// Unsupported content negotiation is reported as a Transport Binding metadata
250/// failure rather than a Core `invalid_request`.
251pub fn validate_http_request<B>(
252    endpoint: &Endpoint,
253    route: Route,
254    value: &Request<B>,
255) -> Result<(), RequestMetadataError> {
256    let expected = endpoint.route(route);
257    if value.uri().path() != expected.path() || value.uri().query().is_some() {
258        return Err(RequestMetadataError::NotFound);
259    }
260    if value.method() != Method::POST {
261        return Err(RequestMetadataError::MethodNotAllowed);
262    }
263    let valid_content_type = value
264        .headers()
265        .get(CONTENT_TYPE)
266        .and_then(|value| value.to_str().ok())
267        .is_some_and(is_json_content_type);
268    if !valid_content_type || !has_supported_content_coding(value.headers()) {
269        return Err(RequestMetadataError::UnsupportedMediaType);
270    }
271    if !accepts_json(value.headers()) {
272        return Err(RequestMetadataError::NotAcceptable);
273    }
274    Ok(())
275}
276
277/// Encodes an `observe` HTTP request.
278pub fn encode_observe_request(
279    endpoint: &Endpoint,
280    value: &ObserveRequest,
281    limits: Limits,
282) -> Result<Request<Vec<u8>>, EncodeHttpError> {
283    value.validate().map_err(CodecError::from)?;
284    let body = json::serialize(
285        &json::observe_request_to_json(value),
286        BodyKind::Request,
287        limits,
288    )?;
289    Ok(request(endpoint, Route::Observe, body)?)
290}
291
292/// Decodes an `observe` request body.
293pub fn decode_observe_request(body: &[u8], limits: Limits) -> Result<ObserveRequest, CodecError> {
294    let value = json::parse(body, BodyKind::Request, limits)?;
295    json::observe_request_from_json(&value)
296}
297
298/// Encodes a `fetch_interface` HTTP request.
299pub fn encode_fetch_interface_request(
300    endpoint: &Endpoint,
301    value: &FetchInterfaceRequest,
302    limits: Limits,
303) -> Result<Request<Vec<u8>>, EncodeHttpError> {
304    let body = json::serialize(
305        &json::fetch_interface_request_to_json(value),
306        BodyKind::Request,
307        limits,
308    )?;
309    Ok(request(endpoint, Route::FetchInterface, body)?)
310}
311
312/// Decodes a `fetch_interface` request body.
313pub fn decode_fetch_interface_request(
314    body: &[u8],
315    limits: Limits,
316) -> Result<FetchInterfaceRequest, CodecError> {
317    let value = json::parse(body, BodyKind::Request, limits)?;
318    json::fetch_interface_request_from_json(&value)
319}
320
321/// Descriptor-independent fields needed to resolve a `call_operation` request.
322///
323/// A Host decodes this metadata first, fixes the current interface descriptor,
324/// then passes the same body and descriptor to [`decode_call_operation_request`].
325/// This preserves canonical path/interface/operation lookup and error precedence
326/// without treating unvalidated argument JSON as a logical [`wip_protocol::Value`].
327#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct CallOperationMetadata {
329    /// Path and optional object-state precondition.
330    pub target: Target,
331    /// Interface reference and optional interface precondition.
332    pub interface: InterfaceTarget,
333    /// Operation name used to select a declaration.
334    pub operation: String,
335}
336
337impl CallOperationMetadata {
338    /// Decodes the descriptor-bound request and verifies that routing metadata
339    /// still matches the values used to resolve `descriptor`.
340    pub fn decode_request(
341        &self,
342        body: &[u8],
343        descriptor: &InterfaceDescriptor,
344        limits: Limits,
345    ) -> Result<CallOperationRequest, CodecError> {
346        let request = decode_call_operation_request(body, descriptor, limits)?;
347        if request.target != self.target
348            || request.interface != self.interface
349            || request.operation != self.operation
350        {
351            return Err(CodecError::InvalidField {
352                field: "request".into(),
353                reason: "call metadata changed after descriptor resolution".into(),
354            });
355        }
356        Ok(request)
357    }
358}
359
360/// Decodes descriptor-independent `call_operation` routing metadata.
361pub fn decode_call_operation_metadata(
362    body: &[u8],
363    limits: Limits,
364) -> Result<CallOperationMetadata, CodecError> {
365    let value = json::parse(body, BodyKind::Request, limits)?;
366    let (target, interface, operation) = json::call_request_metadata_from_json(&value)?;
367    Ok(CallOperationMetadata {
368        target,
369        interface,
370        operation,
371    })
372}
373
374/// Encodes a descriptor-bound `call_operation` HTTP request.
375pub fn encode_call_operation_request(
376    endpoint: &Endpoint,
377    value: &CallOperationRequest,
378    descriptor: &InterfaceDescriptor,
379    limits: Limits,
380) -> Result<Request<Vec<u8>>, EncodeHttpError> {
381    let value = json::call_request_to_json(value, descriptor)?;
382    let body = json::serialize(&value, BodyKind::Request, limits)?;
383    Ok(request(endpoint, Route::CallOperation, body)?)
384}
385
386/// Decodes a descriptor-bound `call_operation` request body.
387pub fn decode_call_operation_request(
388    body: &[u8],
389    descriptor: &InterfaceDescriptor,
390    limits: Limits,
391) -> Result<CallOperationRequest, CodecError> {
392    let value = json::parse(body, BodyKind::Request, limits)?;
393    json::call_request_from_json(&value, descriptor)
394}
395
396/// Encodes a successful `observe` response as a direct body with HTTP 200.
397pub fn encode_observe_response(
398    request_value: &ObserveRequest,
399    value: &ObserveResponse,
400    limits: Limits,
401) -> Result<Response<Vec<u8>>, EncodeHttpError> {
402    request_value
403        .validate_response(value)
404        .map_err(CodecError::from)?;
405    let body = json::serialize(
406        &json::observe_response_to_json(value),
407        BodyKind::Response,
408        limits,
409    )?;
410    Ok(response(StatusCode::OK, body)?)
411}
412
413/// Decodes and classifies an `observe` client response.
414pub fn decode_observe_response<B: AsRef<[u8]>>(
415    request_value: &ObserveRequest,
416    value: &Response<B>,
417    limits: Limits,
418) -> Result<DecodedResponse<ObserveResponse>, ClientResponseError> {
419    require_response_content_type(value)?;
420    response::decode(
421        value.status(),
422        value.body().as_ref(),
423        ProtocolInteraction::Observe,
424        limits,
425        |json| {
426            let observation = json::observe_response_from_json(json)?;
427            request_value.validate_response(&observation)?;
428            Ok(observation)
429        },
430    )
431}
432
433/// Encodes a successful `fetch_interface` response as a direct body with HTTP 200.
434pub fn encode_fetch_interface_response(
435    request_value: &FetchInterfaceRequest,
436    value: &FetchInterfaceResponse,
437    limits: Limits,
438) -> Result<Response<Vec<u8>>, EncodeHttpError> {
439    value
440        .validate_for(request_value)
441        .map_err(CodecError::from)?;
442    let body = json::serialize(
443        &json::fetch_interface_response_to_json(value),
444        BodyKind::Response,
445        limits,
446    )?;
447    Ok(response(StatusCode::OK, body)?)
448}
449
450/// Decodes and classifies a `fetch_interface` client response.
451pub fn decode_fetch_interface_response<B: AsRef<[u8]>>(
452    request_value: &FetchInterfaceRequest,
453    value: &Response<B>,
454    limits: Limits,
455) -> Result<DecodedResponse<FetchInterfaceResponse>, ClientResponseError> {
456    require_response_content_type(value)?;
457    let decoded = response::decode(
458        value.status(),
459        value.body().as_ref(),
460        ProtocolInteraction::FetchInterface,
461        limits,
462        |json| {
463            let interface = json::fetch_interface_response_from_json(json)?;
464            interface.validate_for(request_value)?;
465            Ok(interface)
466        },
467    )?;
468    if let DecodedResponse::Success(success) = &decoded
469        && success.descriptor.format != INTERFACE_FORMAT_V1
470    {
471        return Err(ClientResponseError::UnsupportedDescriptorFormat {
472            format: success.descriptor.format.clone(),
473        });
474    }
475    Ok(decoded)
476}
477
478/// Encodes a successful `call_operation` response as a direct body with HTTP 200.
479pub fn encode_call_operation_response(
480    request_value: &CallOperationRequest,
481    descriptor: &InterfaceDescriptor,
482    value: &CallOperationResponse,
483    limits: Limits,
484) -> Result<Response<Vec<u8>>, EncodeHttpError> {
485    let json = json::call_response_to_json(value, descriptor, &request_value.operation)?;
486    let body = json::serialize(&json, BodyKind::Response, limits)?;
487    Ok(response(StatusCode::OK, body)?)
488}
489
490/// Decodes and classifies a descriptor-bound `call_operation` client response.
491pub fn decode_call_operation_response<B: AsRef<[u8]>>(
492    request_value: &CallOperationRequest,
493    descriptor: &InterfaceDescriptor,
494    value: &Response<B>,
495    limits: Limits,
496) -> Result<DecodedResponse<CallOperationResponse>, ClientResponseError> {
497    require_response_content_type(value)?;
498    response::decode(
499        value.status(),
500        value.body().as_ref(),
501        ProtocolInteraction::CallOperation,
502        limits,
503        |json| json::call_response_from_json(json, descriptor, &request_value.operation),
504    )
505}
506
507fn validate_protocol_error(
508    interaction: ProtocolInteraction,
509    error: &ProtocolError,
510) -> Result<(), CodecError> {
511    if !error.code.is_allowed_for(interaction) {
512        return Err(CodecError::InvalidField {
513            field: "error.code".into(),
514            reason: format!(
515                "code `{}` is not allowed for {interaction:?}",
516                response::error_code_name(error.code)
517            ),
518        });
519    }
520    Ok(())
521}
522
523fn encode_protocol_error_with_status(
524    interaction: ProtocolInteraction,
525    error: &ProtocolError,
526    status: StatusCode,
527    limits: Limits,
528) -> Result<Response<Vec<u8>>, EncodeHttpError> {
529    validate_protocol_error(interaction, error)?;
530    let body = json::serialize(&response::error_to_json(error), BodyKind::Response, limits)?;
531    Ok(response(status, body)?)
532}
533
534/// Encodes a canonical Host error envelope using its standard HTTP status.
535///
536/// The code must be allowed for `interaction`. This API accepts only
537/// [`ProtocolError`], so client-local `InvalidResponse` and
538/// `UnsupportedDescriptorFormat` classifications cannot be emitted as Host
539/// error envelopes. Logical `resource_limit_exceeded` failures use HTTP 422;
540/// representation and deployment limits are not protocol errors.
541pub fn encode_protocol_error_response(
542    interaction: ProtocolInteraction,
543    error: &ProtocolError,
544    limits: Limits,
545) -> Result<Response<Vec<u8>>, EncodeHttpError> {
546    encode_protocol_error_with_status(
547        interaction,
548        error,
549        response::status_for_error(error.code),
550        limits,
551    )
552}
553
554/// Failure while building an outbound framework-neutral HTTP message.
555#[derive(Debug)]
556pub enum EncodeHttpError {
557    /// JSON or protocol encoding failed.
558    Codec(CodecError),
559    /// The framework-neutral HTTP message builder rejected the URI or headers.
560    Http(http::Error),
561}
562
563impl std::fmt::Display for EncodeHttpError {
564    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
565        match self {
566            Self::Codec(error) => error.fmt(formatter),
567            Self::Http(error) => error.fmt(formatter),
568        }
569    }
570}
571
572impl std::error::Error for EncodeHttpError {
573    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
574        match self {
575            Self::Codec(error) => Some(error),
576            Self::Http(error) => Some(error),
577        }
578    }
579}
580
581impl From<CodecError> for EncodeHttpError {
582    fn from(value: CodecError) -> Self {
583        Self::Codec(value)
584    }
585}
586
587impl From<http::Error> for EncodeHttpError {
588    fn from(value: http::Error) -> Self {
589        Self::Http(value)
590    }
591}