use core::fmt;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum HttpError {
InvalidStartLine,
InvalidMethod,
InvalidTarget,
InvalidVersion,
UnsupportedVersion,
InvalidStatus,
StartLineTooLong,
HeadersTooLarge,
TooManyHeaders,
BareCr,
BareLf,
ObsFold,
InvalidHeaderName,
WhitespaceBeforeColon,
InvalidHeaderValue,
MissingHost,
InvalidHost,
InvalidContentLength,
DuplicateContentLength,
ConflictingFraming,
InvalidTransferEncoding,
UnsupportedTransferEncoding,
TransferEncodingInHttp10,
InvalidChunkSize,
InvalidChunkExtension,
InvalidChunkTerminator,
ChunkLineTooLong,
ChunkExtensionsTooLarge,
BodyTooLarge,
UnexpectedEof,
UnexpectedData,
UnexpectedUpgrade,
ReservedHeader,
BodyNotAllowed,
BodyLengthMismatch,
InvalidState(&'static str),
}
impl HttpError {
#[must_use]
pub const fn status(&self) -> Option<u16> {
match self {
Self::StartLineTooLong => Some(414),
Self::HeadersTooLarge | Self::TooManyHeaders => Some(431),
Self::BodyTooLarge => Some(413),
Self::UnsupportedVersion => Some(505),
Self::UnsupportedTransferEncoding => Some(501),
Self::InvalidStartLine
| Self::InvalidMethod
| Self::InvalidTarget
| Self::InvalidVersion
| Self::BareCr
| Self::BareLf
| Self::ObsFold
| Self::InvalidHeaderName
| Self::WhitespaceBeforeColon
| Self::InvalidHeaderValue
| Self::MissingHost
| Self::InvalidHost
| Self::InvalidContentLength
| Self::DuplicateContentLength
| Self::ConflictingFraming
| Self::InvalidTransferEncoding
| Self::TransferEncodingInHttp10
| Self::InvalidChunkSize
| Self::InvalidChunkExtension
| Self::InvalidChunkTerminator
| Self::ChunkLineTooLong
| Self::ChunkExtensionsTooLarge => Some(400),
Self::InvalidStatus
| Self::UnexpectedEof
| Self::UnexpectedData
| Self::UnexpectedUpgrade
| Self::ReservedHeader
| Self::BodyNotAllowed
| Self::BodyLengthMismatch
| Self::InvalidState(_) => None,
}
}
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = match self {
Self::InvalidStartLine => "malformed start line: expected three space-separated parts",
Self::InvalidMethod => "invalid method: must be a token",
Self::InvalidTarget => "invalid request target",
Self::InvalidVersion => "invalid HTTP version: expected HTTP/DIGIT.DIGIT",
Self::UnsupportedVersion => "unsupported HTTP version: only HTTP/1.0 and HTTP/1.1",
Self::InvalidStatus => "invalid status code: expected three digits in 100..=599",
Self::StartLineTooLong => "start line exceeds the configured limit",
Self::HeadersTooLarge => "header section exceeds the configured byte limit",
Self::TooManyHeaders => "more header fields than the configured limit",
Self::BareCr => "carriage return not followed by line feed",
Self::BareLf => "line feed not preceded by carriage return",
Self::ObsFold => "field line starts with whitespace (obsolete line folding)",
Self::InvalidHeaderName => "invalid field name or missing colon",
Self::WhitespaceBeforeColon => "whitespace between field name and colon",
Self::InvalidHeaderValue => "control character in field value",
Self::MissingHost => "HTTP/1.1 request without a Host field",
Self::InvalidHost => "duplicate or malformed Host field",
Self::InvalidContentLength => "Content-Length is not a single decimal number",
Self::DuplicateContentLength => "Content-Length appears more than once",
Self::ConflictingFraming => "both Content-Length and Transfer-Encoding are present",
Self::InvalidTransferEncoding => "Transfer-Encoding must end in a single chunked",
Self::UnsupportedTransferEncoding => "transfer coding other than chunked",
Self::TransferEncodingInHttp10 => "Transfer-Encoding in an HTTP/1.0 message",
Self::InvalidChunkSize => "invalid chunk size",
Self::InvalidChunkExtension => "invalid chunk extension",
Self::InvalidChunkTerminator => "chunk data not followed by CRLF",
Self::ChunkLineTooLong => "chunk-size line exceeds the configured limit",
Self::ChunkExtensionsTooLarge => {
"chunk extensions exceed the header byte limit for the message"
}
Self::BodyTooLarge => "body exceeds the configured limit",
Self::UnexpectedEof => "connection closed in the middle of a message",
Self::UnexpectedData => "data received when no message was expected",
Self::UnexpectedUpgrade => "protocol switch that the request did not ask for",
Self::ReservedHeader => {
"header is managed by the connection; use the builder method instead"
}
Self::BodyNotAllowed => "this status code cannot carry a body",
Self::BodyLengthMismatch => "streamed body length differs from the declared length",
Self::InvalidState(rule) => return write!(f, "invalid call order: {rule}"),
};
f.write_str(msg)
}
}
impl core::error::Error for HttpError {}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn test_status_mapping_covers_request_errors() {
assert_eq!(HttpError::StartLineTooLong.status(), Some(414));
assert_eq!(HttpError::UnsupportedVersion.status(), Some(505));
assert_eq!(HttpError::UnsupportedTransferEncoding.status(), Some(501));
assert_eq!(HttpError::BareCr.status(), Some(400));
assert_eq!(HttpError::UnexpectedEof.status(), None);
}
#[test]
fn test_display_is_actionable() {
assert_eq!(
HttpError::InvalidState("respond called twice").to_string(),
"invalid call order: respond called twice"
);
assert!(
HttpError::ConflictingFraming
.to_string()
.contains("Content-Length")
);
}
}