fix(http): F-Q-008 add HttpError::InvalidArgs for user-input validation failures

http_service::run mapped `Method::from_bytes` failure on a user-supplied
HTTP method to HttpError::Backend with format!("invalid method: {}", …).
Same for HeaderName/HeaderValue validation in build_headers. But that's
user input, not a backend problem — misclassifying as Backend corrupts
retry policies (operators may retry "backend" but not "invalid input").

Add a new HttpError::InvalidArgs(String) variant. Reroute the three
validation paths in http_service (method, header name, header value) to
emit InvalidArgs instead of Backend.

The executor-side `map_http_err` uses Display, so the new variant
surfaces to scripts as "http: invalid method: …" — same format, more
structured behind it.

AUDIT.md anchor: F-Q-008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 21:02:57 +02:00
parent c072d0474c
commit 59d4c043b0
2 changed files with 12 additions and 3 deletions

View File

@@ -205,8 +205,9 @@ impl HttpServiceImpl {
/// Core request path: validate, build headers, follow redirects /// Core request path: validate, build headers, follow redirects
/// manually, read the response body with a cap. /// manually, read the response body with a cap.
async fn run(&self, req: HttpRequest) -> Result<HttpResponse, HttpError> { async fn run(&self, req: HttpRequest) -> Result<HttpResponse, HttpError> {
// F-Q-008: bad method is user input, not a backend problem.
let method = Method::from_bytes(req.method.as_bytes()) let method = Method::from_bytes(req.method.as_bytes())
.map_err(|_| HttpError::Backend(format!("invalid method: {}", req.method)))?; .map_err(|_| HttpError::InvalidArgs(format!("invalid method: {}", req.method)))?;
let mut current = url::Url::parse(&req.url) let mut current = url::Url::parse(&req.url)
.map_err(|e| HttpError::InvalidUrl(format!("{}: {e}", req.url)))?; .map_err(|e| HttpError::InvalidUrl(format!("{}: {e}", req.url)))?;
@@ -336,10 +337,11 @@ fn build_headers(req: &HttpRequest, _url: &url::Url) -> Result<HeaderMap, HttpEr
let mut has_user_agent = false; let mut has_user_agent = false;
let mut has_content_type = false; let mut has_content_type = false;
for (k, v) in &req.headers { for (k, v) in &req.headers {
// F-Q-008: bad header name/value is user input.
let name = HeaderName::from_bytes(k.as_bytes()) let name = HeaderName::from_bytes(k.as_bytes())
.map_err(|_| HttpError::Backend(format!("invalid header name: {k}")))?; .map_err(|_| HttpError::InvalidArgs(format!("invalid header name: {k}")))?;
let value = HeaderValue::from_str(v) let value = HeaderValue::from_str(v)
.map_err(|_| HttpError::Backend(format!("invalid header value for {k}")))?; .map_err(|_| HttpError::InvalidArgs(format!("invalid header value for {k}")))?;
if name == USER_AGENT { if name == USER_AGENT {
has_user_agent = true; has_user_agent = true;
} }

View File

@@ -109,6 +109,13 @@ pub enum HttpError {
#[error("{0}")] #[error("{0}")]
Network(String), Network(String),
/// F-Q-008: user-supplied input failed validation (bad HTTP method
/// name, malformed header name/value). Distinct from `Backend`
/// because operators may safely retry a `Backend` error but should
/// never retry an `InvalidArgs` failure — the input itself is wrong.
#[error("{0}")]
InvalidArgs(String),
/// Anything else the impl wants to surface (still safe to show a /// Anything else the impl wants to surface (still safe to show a
/// script). /// script).
#[error("{0}")] #[error("{0}")]