Skip to main content

net_wire/http3/
headers.rs

1//! Allocation-free semantic validation of QPACK-decoded HTTP/3 field sections.
2
3use core::fmt;
4
5use crate::qpack::field::section::decode::QpackDecodedFieldSection;
6
7use super::codepoints::Http3ErrorCode;
8use super::enums::message::{Http3HeadersContext, Http3HeadersKind};
9
10/// A semantically valid decoded HTTP/3 HEADERS field section.
11///
12/// This preserves the decoded QPACK section, including field ordering and indexing metadata. The
13/// reported size is the RFC 9114 uncompressed field-section size; it is advisory and does not
14/// enforce a peer SETTINGS limit.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct Http3DecodedHeaderSection<'a> {
17    section: QpackDecodedFieldSection<'a>,
18    kind: Http3HeadersKind,
19    field_section_size: u64,
20}
21
22impl<'a> Http3DecodedHeaderSection<'a> {
23    /// Returns the original decoded QPACK field section.
24    pub const fn section(self) -> QpackDecodedFieldSection<'a> {
25        self.section
26    }
27
28    /// Returns the validated HTTP/3 field-section role.
29    pub const fn kind(self) -> Http3HeadersKind {
30        self.kind
31    }
32
33    /// Returns the uncompressed RFC 9114 field-section size without enforcing a SETTINGS limit.
34    pub const fn field_section_size(self) -> u64 {
35        self.field_section_size
36    }
37}
38
39/// A semantically valid decoded HTTP/3 PUSH_PROMISE request field section.
40///
41/// This distinct type deliberately has no [`Http3HeadersKind`] accessor, preventing it from
42/// being supplied to HEADERS stream sequencing accidentally.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct Http3DecodedPushPromise<'a> {
45    section: QpackDecodedFieldSection<'a>,
46    field_section_size: u64,
47}
48
49impl<'a> Http3DecodedPushPromise<'a> {
50    /// Returns the original decoded QPACK field section.
51    pub const fn section(self) -> QpackDecodedFieldSection<'a> {
52        self.section
53    }
54
55    /// Returns the uncompressed RFC 9114 field-section size without enforcing a SETTINGS limit.
56    pub const fn field_section_size(self) -> u64 {
57        self.field_section_size
58    }
59}
60
61/// Validates one decoded HTTP/3 HEADERS field section without decoding or mutating it.
62pub fn analyze_decoded_header_section<'a>(
63    section: QpackDecodedFieldSection<'a>,
64    context: Http3HeadersContext,
65) -> Result<Http3DecodedHeaderSection<'a>, Http3HeaderSectionError> {
66    let (observed, field_section_size) = scan(section, context)?;
67    let kind = match context {
68        Http3HeadersContext::Request {
69            extended_connect_enabled,
70        } => {
71            validate_request(observed, extended_connect_enabled)?;
72            Http3HeadersKind::Request
73        }
74        Http3HeadersContext::Response => validate_response(observed)?,
75        Http3HeadersContext::Trailers => Http3HeadersKind::Trailers,
76    };
77    Ok(Http3DecodedHeaderSection {
78        section,
79        kind,
80        field_section_size,
81    })
82}
83
84/// Validates one decoded HTTP/3 PUSH_PROMISE field section with request syntax.
85///
86/// This performs only section-local HTTP/3 syntax validation; push cacheability and stream-state
87/// policy remain the caller's responsibility.
88pub fn analyze_decoded_push_promise<'a>(
89    section: QpackDecodedFieldSection<'a>,
90    extended_connect_enabled: bool,
91) -> Result<Http3DecodedPushPromise<'a>, Http3HeaderSectionError> {
92    let context = Http3HeadersContext::Request {
93        extended_connect_enabled,
94    };
95    let (observed, field_section_size) = scan(section, context)?;
96    validate_request(observed, extended_connect_enabled)?;
97    Ok(Http3DecodedPushPromise {
98        section,
99        field_section_size,
100    })
101}
102
103#[derive(Clone, Copy)]
104struct FieldObservation<'a> {
105    value: &'a [u8],
106    field_index: usize,
107}
108
109#[derive(Clone, Copy)]
110struct Observed<'a> {
111    method: Option<FieldObservation<'a>>,
112    scheme: Option<FieldObservation<'a>>,
113    authority: Option<FieldObservation<'a>>,
114    path: Option<FieldObservation<'a>>,
115    protocol: Option<FieldObservation<'a>>,
116    status: Option<FieldObservation<'a>>,
117    host: Option<FieldObservation<'a>>,
118}
119
120fn scan<'a>(
121    section: QpackDecodedFieldSection<'a>,
122    context: Http3HeadersContext,
123) -> Result<(Observed<'a>, u64), Http3HeaderSectionError> {
124    let mut observed = Observed {
125        method: None,
126        scheme: None,
127        authority: None,
128        path: None,
129        protocol: None,
130        status: None,
131        host: None,
132    };
133    let mut size = 0_u64;
134    let mut regular_seen = false;
135
136    for (field_index, field) in section.iter().enumerate() {
137        let name = field.name();
138        let value = field.value();
139        size = add_field_size(size, name, value, field_index)?;
140        validate_field_value(value, field_index)?;
141        if name.first() == Some(&b':') {
142            if regular_seen {
143                return Err(Http3HeaderSectionError::PseudoFieldAfterRegular { field_index });
144            }
145            if context == Http3HeadersContext::Trailers {
146                return Err(Http3HeaderSectionError::PseudoFieldInTrailers { field_index });
147            }
148            observe_pseudo(&mut observed, name, value, field_index, context)?;
149        } else {
150            regular_seen = true;
151            validate_regular_name(name, field_index)?;
152            validate_regular_field(name, value, field_index, context, &mut observed)?;
153        }
154    }
155    Ok((observed, size))
156}
157
158fn add_field_size(
159    size: u64,
160    name: &[u8],
161    value: &[u8],
162    field_index: usize,
163) -> Result<u64, Http3HeaderSectionError> {
164    let name_length = u64::try_from(name.len())
165        .map_err(|_| Http3HeaderSectionError::FieldSectionSizeOverflow { field_index })?;
166    let value_length = u64::try_from(value.len())
167        .map_err(|_| Http3HeaderSectionError::FieldSectionSizeOverflow { field_index })?;
168    name_length
169        .checked_add(value_length)
170        .and_then(|length| length.checked_add(32))
171        .and_then(|length| size.checked_add(length))
172        .ok_or(Http3HeaderSectionError::FieldSectionSizeOverflow { field_index })
173}
174
175fn validate_regular_name(name: &[u8], field_index: usize) -> Result<(), Http3HeaderSectionError> {
176    if name.iter().any(|byte| byte.is_ascii_uppercase()) {
177        return Err(Http3HeaderSectionError::UppercaseFieldName { field_index });
178    }
179    if name.is_empty() || !name.iter().all(|byte| is_token(*byte)) {
180        return Err(Http3HeaderSectionError::InvalidFieldName { field_index });
181    }
182    Ok(())
183}
184
185fn validate_field_value(value: &[u8], field_index: usize) -> Result<(), Http3HeaderSectionError> {
186    if matches!(value.first(), Some(b' ' | b'\t'))
187        || matches!(value.last(), Some(b' ' | b'\t'))
188        || value.iter().any(|byte| {
189            *byte == b'\0'
190                || *byte == b'\r'
191                || *byte == b'\n'
192                || (*byte < 0x20 && *byte != b'\t')
193                || *byte == 0x7f
194        })
195    {
196        return Err(Http3HeaderSectionError::InvalidFieldValue { field_index });
197    }
198    Ok(())
199}
200
201fn observe_pseudo<'a>(
202    observed: &mut Observed<'a>,
203    name: &[u8],
204    value: &'a [u8],
205    field_index: usize,
206    context: Http3HeadersContext,
207) -> Result<(), Http3HeaderSectionError> {
208    let slot = match name {
209        b":method" if matches!(context, Http3HeadersContext::Request { .. }) => {
210            &mut observed.method
211        }
212        b":scheme" if matches!(context, Http3HeadersContext::Request { .. }) => {
213            &mut observed.scheme
214        }
215        b":authority" if matches!(context, Http3HeadersContext::Request { .. }) => {
216            &mut observed.authority
217        }
218        b":path" if matches!(context, Http3HeadersContext::Request { .. }) => &mut observed.path,
219        b":protocol" if matches!(context, Http3HeadersContext::Request { .. }) => {
220            &mut observed.protocol
221        }
222        b":status" if context == Http3HeadersContext::Response => &mut observed.status,
223        _ => return Err(Http3HeaderSectionError::InvalidPseudoField { field_index }),
224    };
225    if slot.is_some() {
226        return Err(Http3HeaderSectionError::DuplicatePseudoField { field_index });
227    }
228    *slot = Some(FieldObservation { value, field_index });
229    Ok(())
230}
231
232fn validate_regular_field<'a>(
233    name: &[u8],
234    value: &'a [u8],
235    field_index: usize,
236    context: Http3HeadersContext,
237    observed: &mut Observed<'a>,
238) -> Result<(), Http3HeaderSectionError> {
239    if matches!(
240        name,
241        b"connection" | b"keep-alive" | b"proxy-connection" | b"transfer-encoding" | b"upgrade"
242    ) {
243        return Err(Http3HeaderSectionError::ConnectionSpecificField { field_index });
244    }
245    if name == b"te"
246        && (!matches!(context, Http3HeadersContext::Request { .. }) || !valid_te(value))
247    {
248        return Err(Http3HeaderSectionError::InvalidTe { field_index });
249    }
250    if name == b"host" {
251        if observed.host.is_some() {
252            return Err(Http3HeaderSectionError::DuplicateHost { field_index });
253        }
254        observed.host = Some(FieldObservation { value, field_index });
255    }
256    Ok(())
257}
258
259fn validate_request(
260    observed: Observed<'_>,
261    extended_connect_enabled: bool,
262) -> Result<(), Http3HeaderSectionError> {
263    let method = observed
264        .method
265        .ok_or(Http3HeaderSectionError::MissingPseudoField { name: b":method" })?;
266    if method.value.is_empty() || !method.value.iter().all(|byte| is_token(*byte)) {
267        return Err(Http3HeaderSectionError::InvalidMethod {
268            field_index: method.field_index,
269        });
270    }
271    let protocol = observed.protocol;
272    let extended = protocol.is_some();
273    if let Some(protocol) = protocol {
274        if !extended_connect_enabled || method.value != b"CONNECT" {
275            return Err(Http3HeaderSectionError::InvalidProtocol {
276                field_index: protocol.field_index,
277            });
278        }
279        if protocol.value.is_empty() || !protocol.value.iter().all(|byte| is_token(*byte)) {
280            return Err(Http3HeaderSectionError::InvalidProtocol {
281                field_index: protocol.field_index,
282            });
283        }
284    }
285    if method.value == b"CONNECT" && !extended {
286        let forbidden = match (observed.scheme, observed.path) {
287            (Some(scheme), Some(path)) if scheme.field_index < path.field_index => Some(scheme),
288            (Some(_), Some(path)) => Some(path),
289            (Some(scheme), None) => Some(scheme),
290            (None, Some(path)) => Some(path),
291            (None, None) => None,
292        };
293        if let Some(field) = forbidden {
294            return Err(Http3HeaderSectionError::InvalidConnectPseudoFields {
295                field_index: field.field_index,
296            });
297        }
298        let authority = observed
299            .authority
300            .ok_or(Http3HeaderSectionError::MissingPseudoField {
301                name: b":authority",
302            })?;
303        if !valid_connect_authority(authority.value) {
304            return Err(Http3HeaderSectionError::InvalidAuthority {
305                field_index: authority.field_index,
306            });
307        }
308        validate_connect_host(authority, observed.host)?;
309        return Ok(());
310    }
311    let scheme = observed
312        .scheme
313        .ok_or(Http3HeaderSectionError::MissingPseudoField { name: b":scheme" })?;
314    let path = observed
315        .path
316        .ok_or(Http3HeaderSectionError::MissingPseudoField { name: b":path" })?;
317    if scheme.value.is_empty() || !valid_scheme(scheme.value) {
318        return Err(Http3HeaderSectionError::InvalidScheme {
319            field_index: scheme.field_index,
320        });
321    }
322    if path.value.is_empty()
323        || ((scheme.value == b"http" || scheme.value == b"https")
324            && !((method.value == b"OPTIONS" && path.value == b"*")
325                || path.value.first() == Some(&b'/')))
326    {
327        return Err(Http3HeaderSectionError::InvalidPath {
328            field_index: path.field_index,
329        });
330    }
331    validate_request_authority(
332        observed.authority,
333        observed.host,
334        extended || scheme.value == b"http" || scheme.value == b"https",
335    )
336}
337
338fn validate_request_authority(
339    authority: Option<FieldObservation<'_>>,
340    host: Option<FieldObservation<'_>>,
341    required: bool,
342) -> Result<(), Http3HeaderSectionError> {
343    if let Some(authority) = authority
344        && (authority.value.is_empty() || authority.value.contains(&b'@'))
345    {
346        return Err(Http3HeaderSectionError::InvalidAuthority {
347            field_index: authority.field_index,
348        });
349    }
350    if let Some(host) = host
351        && host.value.is_empty()
352    {
353        return Err(Http3HeaderSectionError::InvalidHost {
354            field_index: host.field_index,
355        });
356    }
357    if required {
358        match (authority, host) {
359            (Some(authority), Some(host)) if authority.value != host.value => {
360                Err(Http3HeaderSectionError::AuthorityHostMismatch {
361                    authority_field_index: authority.field_index,
362                    host_field_index: host.field_index,
363                })
364            }
365            (Some(_), _) | (None, Some(_)) => Ok(()),
366            (None, None) => Err(Http3HeaderSectionError::MissingAuthorityOrHost),
367        }
368    } else {
369        Ok(())
370    }
371}
372
373fn validate_connect_host(
374    authority: FieldObservation<'_>,
375    host: Option<FieldObservation<'_>>,
376) -> Result<(), Http3HeaderSectionError> {
377    let Some(host) = host else {
378        return Ok(());
379    };
380    if host.value.is_empty() {
381        return Err(Http3HeaderSectionError::InvalidHost {
382            field_index: host.field_index,
383        });
384    }
385    if authority.value != host.value {
386        return Err(Http3HeaderSectionError::AuthorityHostMismatch {
387            authority_field_index: authority.field_index,
388            host_field_index: host.field_index,
389        });
390    }
391    Ok(())
392}
393
394fn validate_response(observed: Observed<'_>) -> Result<Http3HeadersKind, Http3HeaderSectionError> {
395    let status_field = observed
396        .status
397        .ok_or(Http3HeaderSectionError::MissingPseudoField { name: b":status" })?;
398    if status_field.value.len() != 3 || !status_field.value.iter().all(|byte| byte.is_ascii_digit())
399    {
400        return Err(Http3HeaderSectionError::InvalidStatus {
401            field_index: status_field.field_index,
402        });
403    }
404    let status = u16::from(status_field.value[0] - b'0') * 100
405        + u16::from(status_field.value[1] - b'0') * 10
406        + u16::from(status_field.value[2] - b'0');
407    match status {
408        100..=199 if status != 101 => Ok(Http3HeadersKind::InformationalResponse),
409        200..=599 => Ok(Http3HeadersKind::FinalResponse),
410        _ => Err(Http3HeaderSectionError::InvalidStatus {
411            field_index: status_field.field_index,
412        }),
413    }
414}
415
416fn valid_te(value: &[u8]) -> bool {
417    let mut item_seen = false;
418    for item in value.split(|byte| *byte == b',') {
419        let item = trim_ows(item);
420        if !item.is_empty() {
421            item_seen = true;
422            if !item.eq_ignore_ascii_case(b"trailers") {
423                return false;
424            }
425        }
426    }
427    item_seen
428}
429
430fn valid_connect_authority(authority: &[u8]) -> bool {
431    if authority.is_empty() || authority.contains(&b'@') {
432        return false;
433    }
434    let (host, port, bracketed) = if authority.first() == Some(&b'[') {
435        let Some(end) = authority.iter().position(|byte| *byte == b']') else {
436            return false;
437        };
438        if end == 1 || authority.get(end + 1) != Some(&b':') {
439            return false;
440        }
441        (&authority[1..end], &authority[end + 2..], true)
442    } else {
443        let Some(port_start) = authority.iter().rposition(|byte| *byte == b':') else {
444            return false;
445        };
446        (
447            &authority[..port_start],
448            &authority[port_start + 1..],
449            false,
450        )
451    };
452    !host.is_empty() && (bracketed || !host.contains(&b':')) && valid_port(port)
453}
454
455fn valid_port(port: &[u8]) -> bool {
456    if port.is_empty() || !port.iter().all(|byte| byte.is_ascii_digit()) {
457        return false;
458    }
459    let mut value = 0_u32;
460    for byte in port {
461        value = match value
462            .checked_mul(10)
463            .and_then(|value| value.checked_add(u32::from(*byte - b'0')))
464        {
465            Some(value) if value <= 65_535 => value,
466            _ => return false,
467        };
468    }
469    true
470}
471
472fn valid_scheme(scheme: &[u8]) -> bool {
473    scheme
474        .first()
475        .is_some_and(|byte| byte.is_ascii_alphabetic())
476        && scheme[1..]
477            .iter()
478            .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'+' | b'-' | b'.'))
479}
480
481fn trim_ows(mut value: &[u8]) -> &[u8] {
482    while matches!(value.first(), Some(b' ' | b'\t')) {
483        value = &value[1..];
484    }
485    while matches!(value.last(), Some(b' ' | b'\t')) {
486        value = &value[..value.len() - 1];
487    }
488    value
489}
490
491const fn is_token(byte: u8) -> bool {
492    byte.is_ascii_alphanumeric()
493        || matches!(
494            byte,
495            b'!' | b'#'
496                | b'$'
497                | b'%'
498                | b'&'
499                | b'\''
500                | b'*'
501                | b'+'
502                | b'-'
503                | b'.'
504                | b'^'
505                | b'_'
506                | b'`'
507                | b'|'
508                | b'~'
509        )
510}
511
512/// Failure to semantically validate a decoded HTTP/3 field section.
513///
514/// `field_index` values identify decoded fields in wire order. They are not byte offsets because
515/// QPACK decoding does not retain a field's encoded wire offset.
516#[derive(Clone, Copy, Debug, Eq, PartialEq)]
517pub enum Http3HeaderSectionError {
518    /// Adding a decoded field's RFC 9114 size contribution overflowed `u64`.
519    FieldSectionSizeOverflow {
520        /// Index of the field whose contribution overflowed.
521        field_index: usize,
522    },
523    /// A regular field name is empty or not an HTTP token.
524    InvalidFieldName {
525        /// Index of the invalid field.
526        field_index: usize,
527    },
528    /// A regular field name contains an uppercase ASCII byte.
529    UppercaseFieldName {
530        /// Index of the invalid field.
531        field_index: usize,
532    },
533    /// A field value contains invalid HTTP field-content or leading/trailing whitespace.
534    InvalidFieldValue {
535        /// Index of the invalid field.
536        field_index: usize,
537    },
538    /// A pseudo-field follows a regular field.
539    PseudoFieldAfterRegular {
540        /// Index of the misplaced pseudo-field.
541        field_index: usize,
542    },
543    /// A trailer section contains a pseudo-field.
544    PseudoFieldInTrailers {
545        /// Index of the forbidden pseudo-field.
546        field_index: usize,
547    },
548    /// A pseudo-field is unknown or forbidden for this section context.
549    InvalidPseudoField {
550        /// Index of the invalid pseudo-field.
551        field_index: usize,
552    },
553    /// A pseudo-field repeats an earlier pseudo-field.
554    DuplicatePseudoField {
555        /// Index of the repeated pseudo-field.
556        field_index: usize,
557    },
558    /// A required pseudo-field is absent.
559    MissingPseudoField {
560        /// Name of the missing pseudo-field.
561        name: &'static [u8],
562    },
563    /// A connection-specific field is forbidden in HTTP/3.
564    ConnectionSpecificField {
565        /// Index of the forbidden field.
566        field_index: usize,
567    },
568    /// A `te` field is forbidden here or does not contain only `trailers` values.
569    InvalidTe {
570        /// Index of the invalid field.
571        field_index: usize,
572    },
573    /// A `host` field repeats an earlier Host field.
574    DuplicateHost {
575        /// Index of the repeated Host field.
576        field_index: usize,
577    },
578    /// A request has neither a nonempty `:authority` nor a nonempty Host field.
579    MissingAuthorityOrHost,
580    /// A `:authority` value is invalid for the request form.
581    InvalidAuthority {
582        /// Index of the invalid `:authority` field.
583        field_index: usize,
584    },
585    /// A Host field is empty.
586    InvalidHost {
587        /// Index of the invalid Host field.
588        field_index: usize,
589    },
590    /// `:authority` and Host are both present but differ.
591    AuthorityHostMismatch {
592        /// Index of the `:authority` field.
593        authority_field_index: usize,
594        /// Index of the Host field.
595        host_field_index: usize,
596    },
597    /// The `:method` pseudo-field is empty or not an HTTP token.
598    InvalidMethod {
599        /// Index of the invalid `:method` field.
600        field_index: usize,
601    },
602    /// The `:scheme` pseudo-field is empty or malformed.
603    InvalidScheme {
604        /// Index of the invalid `:scheme` field.
605        field_index: usize,
606    },
607    /// The `:path` pseudo-field is empty or invalid for an HTTP scheme.
608    InvalidPath {
609        /// Index of the invalid `:path` field.
610        field_index: usize,
611    },
612    /// The `:protocol` pseudo-field is disabled, malformed, or not paired with CONNECT.
613    InvalidProtocol {
614        /// Index of the invalid `:protocol` field.
615        field_index: usize,
616    },
617    /// An ordinary CONNECT request contains `:scheme` or `:path`.
618    InvalidConnectPseudoFields {
619        /// Index of the first forbidden pseudo-field.
620        field_index: usize,
621    },
622    /// The `:status` pseudo-field is malformed, forbidden, or outside the accepted range.
623    InvalidStatus {
624        /// Index of the invalid `:status` field.
625        field_index: usize,
626    },
627}
628
629impl Http3HeaderSectionError {
630    /// Returns the HTTP/3 application error code required by every semantic field-section error.
631    pub const fn error_code(self) -> Http3ErrorCode {
632        Http3ErrorCode::MESSAGE_ERROR
633    }
634}
635
636impl fmt::Display for Http3HeaderSectionError {
637    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638        match self {
639            Self::FieldSectionSizeOverflow { field_index } => write!(
640                f,
641                "HTTP/3 field {field_index} makes the field-section size overflow"
642            ),
643            Self::InvalidFieldName { field_index } => {
644                write!(f, "HTTP/3 field {field_index} has an invalid name")
645            }
646            Self::UppercaseFieldName { field_index } => {
647                write!(f, "HTTP/3 field {field_index} has an uppercase name")
648            }
649            Self::InvalidFieldValue { field_index } => {
650                write!(f, "HTTP/3 field {field_index} has an invalid value")
651            }
652            Self::PseudoFieldAfterRegular { field_index } => write!(
653                f,
654                "HTTP/3 pseudo-field {field_index} follows a regular field"
655            ),
656            Self::PseudoFieldInTrailers { field_index } => {
657                write!(f, "HTTP/3 trailer pseudo-field {field_index} is forbidden")
658            }
659            Self::InvalidPseudoField { field_index } => write!(
660                f,
661                "HTTP/3 pseudo-field {field_index} is invalid for this context"
662            ),
663            Self::DuplicatePseudoField { field_index } => write!(
664                f,
665                "HTTP/3 pseudo-field {field_index} duplicates an earlier pseudo-field"
666            ),
667            Self::MissingPseudoField { name } => write!(
668                f,
669                "HTTP/3 field section is missing required pseudo-field {name:?}"
670            ),
671            Self::ConnectionSpecificField { field_index } => {
672                write!(f, "HTTP/3 field {field_index} is connection-specific")
673            }
674            Self::InvalidTe { field_index } => {
675                write!(f, "HTTP/3 TE field {field_index} is invalid")
676            }
677            Self::DuplicateHost { field_index } => write!(
678                f,
679                "HTTP/3 Host field {field_index} duplicates an earlier Host field"
680            ),
681            Self::MissingAuthorityOrHost => {
682                f.write_str("HTTP/3 request is missing :authority and Host")
683            }
684            Self::InvalidAuthority { field_index } => {
685                write!(f, "HTTP/3 :authority field {field_index} is invalid")
686            }
687            Self::InvalidHost { field_index } => {
688                write!(f, "HTTP/3 Host field {field_index} is invalid")
689            }
690            Self::AuthorityHostMismatch {
691                authority_field_index,
692                host_field_index,
693            } => write!(
694                f,
695                "HTTP/3 :authority field {authority_field_index} and Host field {host_field_index} differ"
696            ),
697            Self::InvalidMethod { field_index } => {
698                write!(f, "HTTP/3 :method field {field_index} is invalid")
699            }
700            Self::InvalidScheme { field_index } => {
701                write!(f, "HTTP/3 :scheme field {field_index} is invalid")
702            }
703            Self::InvalidPath { field_index } => {
704                write!(f, "HTTP/3 :path field {field_index} is invalid")
705            }
706            Self::InvalidProtocol { field_index } => {
707                write!(f, "HTTP/3 :protocol field {field_index} is invalid")
708            }
709            Self::InvalidConnectPseudoFields { field_index } => write!(
710                f,
711                "HTTP/3 ordinary CONNECT pseudo-field {field_index} is forbidden"
712            ),
713            Self::InvalidStatus { field_index } => {
714                write!(f, "HTTP/3 :status field {field_index} is invalid")
715            }
716        }
717    }
718}
719
720impl core::error::Error for Http3HeaderSectionError {}