Skip to main content

ureq_proto/
error.rs

1use std::fmt;
2
3use http::{Method, StatusCode, Version};
4
5/// Error type for ureq-proto
6#[derive(Debug, PartialEq, Eq)]
7#[allow(missing_docs)]
8#[non_exhaustive]
9pub enum Error {
10    BadHeader(String),
11    UnsupportedVersion,
12    MethodVersionMismatch(Method, Version),
13    TooManyHostHeaders,
14    TooManyContentLengthHeaders,
15    BadHostHeader,
16    BadAuthorizationHeader,
17    BadContentLengthHeader,
18    OutputOverflow,
19    ChunkLenNotAscii,
20    ChunkLenNotANumber,
21    ChunkExpectedCrLf,
22    BodyContentAfterFinish,
23    BodyLargerThanContentLength,
24    BodyNotAllowed,
25    HttpParseFail(String),
26    HttpParseTooManyHeaders,
27    NoLocationHeader,
28    BadLocationHeader(String),
29    HeadersWith100,
30    BodyIsChunked,
31    BadReject100Status(StatusCode),
32}
33
34impl From<httparse::Error> for Error {
35    fn from(value: httparse::Error) -> Self {
36        Error::HttpParseFail(value.to_string())
37    }
38}
39
40impl std::error::Error for Error {}
41
42impl fmt::Display for Error {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Error::BadHeader(v) => write!(f, "bad header: {}", v),
46            Error::UnsupportedVersion => write!(f, "unsupported http version"),
47            Error::MethodVersionMismatch(m, v) => {
48                write!(f, "{} not valid for HTTP version {:?}", m, v)
49            }
50            Error::TooManyHostHeaders => write!(f, "more than one host header"),
51            Error::TooManyContentLengthHeaders => write!(f, "conflicting content-length headers"),
52            Error::BadHostHeader => write!(f, "host header is not a string"),
53            Error::BadAuthorizationHeader => write!(f, "authorization header is not a string"),
54            Error::BadContentLengthHeader => write!(f, "content-length header not a number"),
55            Error::OutputOverflow => write!(f, "output too small to write output"),
56            Error::ChunkLenNotAscii => write!(f, "chunk length is not ascii"),
57            Error::ChunkLenNotANumber => write!(f, "chunk length cannot be read as a number"),
58            Error::ChunkExpectedCrLf => write!(f, "chunk expected crlf as next character"),
59            Error::BodyContentAfterFinish => {
60                write!(f, "attempt to stream body after sending finish (&[])")
61            }
62            Error::BodyLargerThanContentLength => {
63                write!(f, "attempt to write larger body than content-length")
64            }
65            Error::BodyNotAllowed => {
66                write!(f, "body not allowed by method")
67            }
68            Error::HttpParseFail(v) => write!(f, "http parse fail: {}", v),
69            Error::HttpParseTooManyHeaders => write!(f, "http parse resulted in too many headers"),
70            Error::NoLocationHeader => write!(f, "missing a location header"),
71            Error::BadLocationHeader(v) => write!(f, "location header is malformed: {}", v),
72            Error::HeadersWith100 => write!(f, "received headers with 100-continue response"),
73            Error::BodyIsChunked => write!(f, "body is chunked"),
74            Error::BadReject100Status(v) => {
75                write!(f, "expect-100 must be rejected with 4xx or 5xx: {}", v)
76            }
77        }
78    }
79}
80
81#[cfg(test)]
82#[cfg(feature = "client")]
83mod tests_client {
84    use super::*;
85    use crate::client::{
86        Call, RecvResponseResult, RedirectAuthHeaders, SendRequestResult,
87        state::{RecvResponse, Redirect, SendBody, SendRequest},
88    };
89    use http::{HeaderValue, Method, Request, Version};
90
91    // Helper methods to reduce test verbosity
92
93    /// Creates a Call object from a request and proceeds to the initial state
94    fn setup_call(req: Request<()>) -> (Call<SendRequest>, Vec<u8>) {
95        let call = Call::new(req).unwrap();
96        let call = call.proceed();
97        let output = vec![0; 1024];
98        (call, output)
99    }
100
101    /// Creates a Call object and proceeds to the RecvResponse state
102    fn setup_recv_response_call() -> (Call<RecvResponse>, Vec<u8>) {
103        let req = Request::get("http://example.com").body(()).unwrap();
104        let (mut call, mut output) = setup_call(req);
105
106        // Write the request headers
107        call.write(&mut output).unwrap();
108
109        // Proceed to RecvResponse
110        let call = call.proceed().unwrap().unwrap();
111        let SendRequestResult::RecvResponse(call) = call else {
112            panic!("Expected SendRequestResult::RecvResponse");
113        };
114
115        (call, output)
116    }
117
118    /// Creates a Call object and proceeds to the SendBody state
119    fn setup_send_body_call(req: Request<()>) -> (Call<SendBody>, Vec<u8>, usize) {
120        let (mut call, mut output) = setup_call(req);
121
122        // Write the request headers
123        let n = call.write(&mut output).unwrap();
124
125        // Proceed to SendBody
126        let call = call.proceed().unwrap().unwrap();
127        let SendRequestResult::SendBody(call) = call else {
128            panic!("Expected SendBody");
129        };
130
131        (call, output, n)
132    }
133
134    /// Creates a Call object and proceeds to the Redirect state with the given response
135    fn setup_redirect_call(response: &[u8]) -> Result<(Call<Redirect>, Vec<u8>), Error> {
136        let (mut call, output) = setup_recv_response_call();
137
138        // Try to parse the response
139        let (_, _) = call.try_response(response, false)?;
140
141        // Proceed to Redirect state
142        let RecvResponseResult::Redirect(call) = call.proceed().unwrap() else {
143            panic!("Expected RecvResponseResult::Redirect");
144        };
145
146        Ok((call, output))
147    }
148
149    // BadHeader
150    #[test]
151    fn test_bad_header() {
152        // Create a request
153        let req = Request::get("http://example.com").body(()).unwrap();
154        let mut call = Call::new(req).unwrap();
155
156        // Try to set an invalid header using the Call API
157        let result = call.header("Invalid\0Header", "value");
158
159        // Verify that it returns a BadHeader error
160        assert!(result.is_err());
161        let err = result.unwrap_err();
162        assert!(matches!(err, Error::BadHeader(_)));
163    }
164
165    // UnsupportedVersion
166    #[test]
167    fn test_unsupported_version() {
168        // Create a request with HTTP/2.0 which is not supported by the Call API
169        let req = Request::builder()
170            .uri("http://example.com")
171            .version(Version::HTTP_2)
172            .body(())
173            .unwrap();
174
175        let (mut call, mut output) = setup_call(req);
176
177        // Try to write the request headers
178        let err = call.write(&mut output).unwrap_err();
179
180        assert!(matches!(err, Error::UnsupportedVersion));
181    }
182
183    // MethodVersionMismatch
184    #[test]
185    fn test_method_version_mismatch() {
186        // TRACE method is not valid for HTTP/1.0
187        let m = Method::from_bytes(b"TRACE").unwrap();
188        let req = Request::builder()
189            .method(m.clone())
190            .uri("http://example.com")
191            .version(Version::HTTP_10)
192            .body(())
193            .unwrap();
194
195        let (mut call, mut output) = setup_call(req);
196
197        // Try to write the request headers
198        let err = call.write(&mut output).unwrap_err();
199
200        assert!(matches!(err, Error::MethodVersionMismatch(_, _)));
201        if let Error::MethodVersionMismatch(method, version) = err {
202            assert_eq!(method, m);
203            assert_eq!(version, Version::HTTP_10);
204        }
205    }
206
207    // TooManyHostHeaders
208    #[test]
209    fn test_too_many_host_headers() {
210        // Create a request with two Host headers
211        let req = Request::builder()
212            .uri("http://example.com")
213            .header("Host", "example.com")
214            .header("Host", "another-example.com")
215            .body(())
216            .unwrap();
217
218        let (mut call, mut output) = setup_call(req);
219
220        // Try to write the request headers
221        let err = call.write(&mut output).unwrap_err();
222
223        // Verify that it returns a TooManyHostHeaders error
224        assert!(matches!(err, Error::TooManyHostHeaders));
225    }
226
227    // BadContentLengthHeader
228    #[test]
229    fn test_signed_content_length_header() {
230        // Content-Length must be plain digits. Rust's integer parsing would
231        // accept a leading sign, the HTTP grammar does not.
232        let req = Request::builder()
233            .uri("http://example.com")
234            .header("Content-Length", "+10")
235            .body(())
236            .unwrap();
237
238        let (mut call, mut output) = setup_call(req);
239
240        // Try to write the request headers
241        let err = call.write(&mut output).unwrap_err();
242
243        assert!(matches!(err, Error::BadContentLengthHeader));
244    }
245
246    // BadContentLengthHeader
247    #[test]
248    fn test_list_content_length_header() {
249        // We control what we send. A comma separated list is not a valid
250        // Content-Length to send (RFC 9110 ยง8.6: the value is 1*DIGIT), so
251        // it must be rejected here rather than written to the wire verbatim.
252        let req = Request::builder()
253            .uri("http://example.com")
254            .header("Content-Length", "42, 42")
255            .body(())
256            .unwrap();
257
258        let (mut call, mut output) = setup_call(req);
259
260        // Try to write the request headers
261        let err = call.write(&mut output).unwrap_err();
262
263        assert!(matches!(
264            err,
265            Error::BadContentLengthHeader | Error::TooManyContentLengthHeaders
266        ));
267    }
268
269    // TooManyContentLengthHeaders
270    #[test]
271    fn test_too_many_content_length_headers() {
272        // Create a request with two Content-Length headers
273        let req = Request::builder()
274            .uri("http://example.com")
275            .header("Content-Length", "10")
276            .header("Content-Length", "20")
277            .body(())
278            .unwrap();
279
280        let (mut call, mut output) = setup_call(req);
281
282        // Try to write the request headers
283        let err = call.write(&mut output).unwrap_err();
284
285        // Verify that it returns a TooManyContentLengthHeaders error
286        assert!(matches!(err, Error::TooManyContentLengthHeaders));
287    }
288
289    // BadHostHeader
290    #[test]
291    fn test_bad_host_header() {
292        // Create a request with an invalid Host header value (non-UTF8 bytes)
293        let req = Request::builder()
294            .uri("http://example.com")
295            .header("Host", HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap())
296            .body(())
297            .unwrap();
298
299        let (mut call, mut output) = setup_call(req);
300
301        // Try to write the request headers
302        let err = call.write(&mut output).unwrap_err();
303
304        // Verify that it returns a BadHostHeader error
305        assert!(matches!(err, Error::BadHostHeader));
306    }
307
308    // BadAuthorizationHeader
309    #[test]
310    fn test_bad_authorization_header() {
311        // Create a request with an invalid Authorization header value (non-UTF8 bytes)
312        let req = Request::builder()
313            .uri("http://example.com")
314            .header(
315                "Authorization",
316                HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap(),
317            )
318            .body(())
319            .unwrap();
320
321        let (mut call, mut output) = setup_call(req);
322
323        // Try to write the request headers
324        let err = call.write(&mut output).unwrap_err();
325
326        // Verify that it returns a BadAuthorizationHeader error
327        assert!(matches!(err, Error::BadAuthorizationHeader));
328    }
329
330    // BadContentLengthHeader
331    #[test]
332    fn test_bad_content_length_header() {
333        // Create a request with an invalid Content-Length header value (not a number)
334        let req = Request::builder()
335            .uri("http://example.com")
336            .header("Content-Length", "not-a-number")
337            .body(())
338            .unwrap();
339
340        let (mut call, mut output) = setup_call(req);
341
342        // Try to write the request headers
343        let err = call.write(&mut output).unwrap_err();
344
345        // Verify that it returns a BadContentLengthHeader error
346        assert!(matches!(err, Error::BadContentLengthHeader));
347    }
348
349    // OutputOverflow
350    #[test]
351    fn test_output_overflow() {
352        // Create a request with a long header
353        let req = Request::builder()
354            .uri("http://example.com")
355            .header("x-long-header", "a".repeat(100))
356            .body(())
357            .unwrap();
358
359        let (mut call, _) = setup_call(req);
360
361        // Try to write to a very small output buffer
362        let mut tiny_output = vec![0; 10];
363        let err = call.write(&mut tiny_output).unwrap_err();
364
365        assert!(matches!(err, Error::OutputOverflow));
366    }
367
368    /// Tests a chunked encoding error with the given chunk data
369    fn test_chunk_error(chunk_data: &[u8]) -> Error {
370        let (mut call, mut output) = setup_recv_response_call();
371
372        const RES_PREFIX: &[u8] = b"HTTP/1.1 200 OK\r\n\
373                Transfer-Encoding: chunked\r\n\
374                \r\n";
375
376        call.try_response(RES_PREFIX, false).unwrap();
377
378        let RecvResponseResult::RecvBody(mut call) = call.proceed().unwrap() else {
379            panic!("Expected RecvResponseResult::RecvBody");
380        };
381
382        call.read(chunk_data, &mut output).unwrap_err()
383    }
384
385    // ChunkLenNotAscii
386    #[test]
387    fn test_chunk_len_not_ascii() {
388        let err = test_chunk_error(b"\xFF\r\ndata\r\n");
389        assert!(matches!(err, Error::ChunkLenNotAscii));
390    }
391
392    // ChunkLenNotANumber
393    #[test]
394    fn test_chunk_len_not_a_number() {
395        let err = test_chunk_error(b"xyz\r\ndata\r\n");
396        assert!(matches!(err, Error::ChunkLenNotANumber));
397    }
398
399    // ChunkExpectedCrLf
400    #[test]
401    fn test_chunk_expected_crlf() {
402        let err = test_chunk_error(b"5abcdefghijabcdefghijabcdefghij\r\n");
403        assert!(matches!(err, Error::ChunkExpectedCrLf));
404    }
405
406    // BodyContentAfterFinish
407    #[test]
408    fn test_body_content_after_finish() {
409        // Create a POST request
410        let req = Request::post("http://example.com").body(()).unwrap();
411
412        let (mut call, mut output, n) = setup_send_body_call(req);
413
414        // Write some data
415        let (_, n2) = call.write(b"data", &mut output[n..]).unwrap();
416
417        // Finish the body
418        let (_, n3) = call.write(&[], &mut output[n + n2..]).unwrap();
419
420        // Try to write more data after finishing
421        let err = call
422            .write(b"more data", &mut output[n + n2 + n3..])
423            .unwrap_err();
424        assert!(matches!(err, Error::BodyContentAfterFinish));
425    }
426
427    // BodyLargerThanContentLength
428    #[test]
429    fn test_body_larger_than_content_length() {
430        // Create a request with a content-length header
431        let req = Request::post("http://example.com")
432            .header("content-length", "5")
433            .body(())
434            .unwrap();
435
436        let (mut call, mut output, n) = setup_send_body_call(req);
437
438        // Try to write more data than specified in content-length
439        let err = call.write(b"too much data", &mut output[n..]).unwrap_err();
440        assert!(matches!(err, Error::BodyLargerThanContentLength));
441    }
442
443    // HttpParseFail
444    #[test]
445    fn test_http_parse_fail() {
446        let (mut call, _) = setup_recv_response_call();
447
448        // Invalid HTTP response (missing space after HTTP/1.1)
449        const RES: &[u8] = b"HTTP/1.1200 OK\r\n\r\n";
450
451        let err = call.try_response(RES, false).unwrap_err();
452
453        assert!(matches!(err, Error::HttpParseFail(_)));
454    }
455
456    // HttpParseTooManyHeaders
457    #[test]
458    fn test_http_parse_too_many_headers() {
459        let (mut call, _) = setup_recv_response_call();
460
461        // Create a response with many headers (more than the parser can handle)
462        let mut res = String::from("HTTP/1.1 200 OK\r\n");
463        for i in 0..1000 {
464            res.push_str(&format!("X-Header-{}: value\r\n", i));
465        }
466        res.push_str("\r\n");
467
468        let err = call.try_response(res.as_bytes(), false).unwrap_err();
469
470        assert!(matches!(err, Error::HttpParseTooManyHeaders));
471    }
472
473    // NoLocationHeader
474    #[test]
475    fn test_no_location_header() {
476        // Redirect response without a Location header
477        const RES: &[u8] = b"HTTP/1.1 302 Found\r\n\
478            \r\n";
479
480        let (mut call, _) = setup_redirect_call(RES).unwrap();
481
482        // Try to create a new Call, which should fail due to missing Location header
483        let err = call.as_new_call(RedirectAuthHeaders::Never).unwrap_err();
484
485        assert!(matches!(err, Error::NoLocationHeader));
486    }
487
488    // BadLocationHeader
489    #[test]
490    fn test_bad_location_header() {
491        // Redirect response with a malformed Location header
492        const RES: &[u8] = b"HTTP/1.1 302 Found\r\n\
493            Location: \xFF\r\n\
494            \r\n";
495
496        let (mut call, _) = setup_redirect_call(RES).unwrap();
497
498        // Try to create a new Call, which should fail due to malformed Location header
499        let err = call.as_new_call(RedirectAuthHeaders::Never).unwrap_err();
500
501        assert!(matches!(err, Error::BadLocationHeader(_)));
502    }
503
504    // HeadersWith100
505    #[test]
506    fn test_headers_with_100() {
507        let (mut call, _) = setup_recv_response_call();
508
509        // 100 Continue response with headers
510        const RES: &[u8] = b"HTTP/1.1 100 Continue\r\n\
511            Content-Type: text/plain\r\n\
512            \r\n";
513
514        let err = call.try_response(RES, false).unwrap_err();
515
516        assert!(matches!(err, Error::HeadersWith100));
517    }
518
519    // BodyIsChunked
520    #[test]
521    fn test_body_is_chunked() {
522        // Create a request with chunked transfer encoding
523        let req = Request::post("http://example.com")
524            .header("transfer-encoding", "chunked")
525            .body(())
526            .unwrap();
527
528        let (mut call, _, _) = setup_send_body_call(req);
529
530        // Try to use consume_direct_write which doesn't work with chunked encoding
531        let err = call.consume_direct_write(5).unwrap_err();
532        assert!(matches!(err, Error::BodyIsChunked));
533    }
534
535    // Test the From<httparse::Error> implementation
536    #[test]
537    fn test_from_httparse_error() {
538        let httparse_error = httparse::Error::HeaderName;
539        let error: Error = httparse_error.into();
540        assert!(matches!(error, Error::HttpParseFail(_)));
541        let Error::HttpParseFail(_) = error else {
542            panic!("Not Error::HttpParseFail");
543        };
544    }
545}