diff --git a/AGENTS.md b/AGENTS.md index 1a836a4..a4fb6a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,7 +121,7 @@ metadata/update/DNS/TLS inspection modes, and executes requests via `src/http`. - Response compression negotiation is controlled by `--compress auto|br|gzip|zstd|off` or `compress = ...`; `brotli` is accepted as an alias for `br`, `auto` requests and decodes gzip/brotli/zstd, single-algorithm modes only request/decode that algorithm, and `off` leaves compressed bodies untouched. - Formatted SSE responses stream incrementally to stdout with terminal color when enabled, rendering events as `event:`/`data:` blocks while formatting JSON data. Auto-compressed SSE responses are retried without `Accept-Encoding` so intermediaries do not buffer events; request timeouts from flags, curl commands, or config remain enforced. - Formatted NDJSON responses stream incrementally to stdout when formatting is enabled, splitting decoded bytes on newlines, formatting each record with the JSON-line formatter, and flushing after each record. -- Digest authentication retries drain oversized 401 challenge bodies with a fixed bound. On Windows, keep-alive challenge responses are retried before the challenge response is dropped so the local TCP abort from abandoning the first response cannot poison the follow-up request; oversized `Connection: close` challenges are drained to the bound and dropped before retrying on a fresh connection. Unsupported or malformed Digest challenges surface an explicit diagnostic before the body replay check. +- Digest authentication retries drain oversized 401 challenge bodies with a fixed bound. On Windows, challenge responses that may be abandoned before EOF are retried before the first response is dropped so the local TCP abort from abandoning that response cannot poison the follow-up request. Unsupported or malformed Digest challenges surface an explicit diagnostic before the body replay check. - `--sort-headers` or `sort-headers = true` sorts displayed request/response headers alphabetically by name in verbose output without changing the actual request header order. - Default HTTP requests send `Accept: application/json, */*;q=0.5`, preferring JSON while allowing any other response type as a lower-priority fallback. - `--basic` and `--digest` credentials preserve exact bytes around the first colon; leading/trailing spaces in usernames or passwords are significant and are not trimmed after CLI or `--from-curl` parsing. diff --git a/src/http/mod.rs b/src/http/mod.rs index b07b68f..e26fc14 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1819,15 +1819,10 @@ async fn apply_digest_challenge( if response_body_exceeds_discard_bound(&response) { drain_response_body_bounded_mut(&mut response).await; - if digest_drop_before_retry_after_large_drain(&response) { - drop(response); - retry_request.send().await.map_err(Into::into) - } else { - let retry_response: Result = - retry_request.send().await.map_err(Into::into); - drop(response); - retry_response - } + let retry_response: Result = + retry_request.send().await.map_err(Into::into); + drop(response); + retry_response } else if digest_retry_before_drain(&response) { let retry_response: Result = retry_request.send().await.map_err(Into::into); @@ -1849,19 +1844,6 @@ fn digest_challenge_error(err: digest::DigestError) -> FetchError { FetchError::Runtime(format!("{prefix} digest authentication challenge: {err}")) } -fn digest_drop_before_retry_after_large_drain(response: &Response) -> bool { - #[cfg(windows)] - { - response_connection_close(response) - } - - #[cfg(not(windows))] - { - let _ = response; - false - } -} - fn digest_retry_before_drain(response: &Response) -> bool { if response_body_exceeds_discard_bound(response) { return true; diff --git a/src/output/mod.rs b/src/output/mod.rs index 2ada2cb..0523323 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -580,7 +580,9 @@ fn parse_content_disposition_filename(value: &str) -> Option { let mut filename_star = None; for part in split_parameters(value).into_iter().skip(1) { - let (key, raw_value) = part.split_once('=')?; + let Some((key, raw_value)) = part.split_once('=') else { + continue; + }; let key = key.trim(); let value = parse_parameter_value(raw_value.trim()); if key.eq_ignore_ascii_case("filename") { @@ -882,6 +884,14 @@ mod tests { ); } + #[test] + fn content_disposition_filename_skips_malformed_parameters() { + assert_eq!( + parse_content_disposition_filename(r#"attachment; bad-param; filename="ok.txt""#), + Some("ok.txt".to_string()) + ); + } + #[tokio::test] async fn write_output_overwrites_existing_file_with_clobber() { let dir = tempfile::tempdir().unwrap();