@@ -526,6 +526,11 @@ fn test_http_response_type_codec() {
526526 "GET" . to_string( ) ,
527527 "/v2/neighbors" . to_string( ) ,
528528 ) ,
529+ (
530+ StacksHttpResponse :: new_empty_error( & * http_error_from_code_and_text( 405 , "" . into( ) ) ) ,
531+ "GET" . to_string( ) ,
532+ "/v2/neighbors" . to_string( ) ,
533+ ) ,
529534 (
530535 StacksHttpResponse :: new_empty_error( & * http_error_from_code_and_text( 500 , "" . into( ) ) ) ,
531536 "GET" . to_string( ) ,
@@ -567,6 +572,11 @@ fn test_http_response_type_codec() {
567572 "GET" . to_string( ) ,
568573 "/v2/neighbors" . to_string( ) ,
569574 ) ,
575+ (
576+ StacksHttpResponse :: new_empty_error( & * http_error_from_code_and_text( 405 , "foo" . into( ) ) ) ,
577+ "GET" . to_string( ) ,
578+ "/v2/neighbors" . to_string( ) ,
579+ ) ,
570580 (
571581 StacksHttpResponse :: new_empty_error( & * http_error_from_code_and_text( 500 , "foo" . into( ) ) ) ,
572582 "GET" . to_string( ) ,
@@ -656,6 +666,7 @@ fn test_http_response_type_codec() {
656666 HttpResponsePreamble :: error_text( 402 , http_reason( 402 ) , "" ) ,
657667 HttpResponsePreamble :: error_text( 403 , http_reason( 403 ) , "" ) ,
658668 HttpResponsePreamble :: error_text( 404 , http_reason( 404 ) , "" ) ,
669+ HttpResponsePreamble :: error_text( 405 , http_reason( 405 ) , "" ) ,
659670 HttpResponsePreamble :: error_text( 500 , http_reason( 500 ) , "" ) ,
660671 HttpResponsePreamble :: error_text( 503 , http_reason( 503 ) , "" ) ,
661672 // generic error
@@ -666,6 +677,7 @@ fn test_http_response_type_codec() {
666677 HttpResponsePreamble :: error_text( 402 , http_reason( 402 ) , "foo" ) ,
667678 HttpResponsePreamble :: error_text( 403 , http_reason( 403 ) , "foo" ) ,
668679 HttpResponsePreamble :: error_text( 404 , http_reason( 404 ) , "foo" ) ,
680+ HttpResponsePreamble :: error_text( 405 , http_reason( 405 ) , "foo" ) ,
669681 HttpResponsePreamble :: error_text( 500 , http_reason( 500 ) , "foo" ) ,
670682 HttpResponsePreamble :: error_text( 503 , http_reason( 503 ) , "foo" ) ,
671683 // generic error
@@ -689,7 +701,8 @@ fn test_http_response_type_codec() {
689701 test_block_info. serialize_to_vec( ) ,
690702 test_microblock_info_bytes. clone( ) ,
691703 Txid ( [ 0x1 ; 32 ] ) . to_hex( ) . as_bytes( ) . to_vec( ) ,
692- // errors
704+ // errors (400, 401, 402, 403, 404, 405, 500, 503, 502)
705+ vec![ ] ,
693706 vec![ ] ,
694707 vec![ ] ,
695708 vec![ ] ,
@@ -707,6 +720,7 @@ fn test_http_response_type_codec() {
707720 "foo" . as_bytes( ) . to_vec( ) ,
708721 "foo" . as_bytes( ) . to_vec( ) ,
709722 "foo" . as_bytes( ) . to_vec( ) ,
723+ "foo" . as_bytes( ) . to_vec( ) ,
710724 ] ;
711725
712726 for ( ( test, request_verb, request_path) , ( expected_http_preamble, _expected_http_body) ) in
@@ -1219,6 +1233,7 @@ fn test_http_response_is_success() {
12191233 ( 401 , false ) , // Unauthorized
12201234 ( 403 , false ) , // Forbidden
12211235 ( 404 , false ) , // Not Found
1236+ ( 405 , false ) , // Method Not Allowed
12221237 ( 418 , false ) , // I'm a teapot
12231238 ( 429 , false ) , // Too Many Requests
12241239 ( 499 , false ) , // Any other 4xx
@@ -1292,3 +1307,60 @@ fn test_send_request_success() {
12921307 "Expected a successful request, but got {result:?}"
12931308 ) ;
12941309}
1310+
1311+ #[ test]
1312+ fn test_http_error_responses ( ) {
1313+ let valid_block_path = format ! ( "/v3/blocks/{}" , "0" . repeat( 64 ) ) ;
1314+
1315+ // (verb, path, expected_status, allow_header_should_contain)
1316+ let fixtures: Vec < ( & str , & str , u16 , Option < & str > ) > = vec ! [
1317+ // 404: nonexistent path
1318+ ( "GET" , "/nonexistent/path" , 404 , None ) ,
1319+ // 405: wrong method on existing path (should have Allow header)
1320+ ( "DELETE" , "/v2/info" , 405 , Some ( "GET" ) ) ,
1321+ // 400: path structure matches but parameters invalid (permissive regex)
1322+ ( "GET" , "/v3/blocks/invalid_block_id" , 400 , None ) ,
1323+ ( "GET" , "/v3/tenures/invalid" , 400 , None ) ,
1324+ // Valid block_id format should NOT return 400
1325+ ( "GET" , & valid_block_path, 200 , None ) ,
1326+ ] ;
1327+
1328+ for ( verb, path, expected_status, allow_contains) in fixtures {
1329+ let addr = "127.0.0.1:20443" . parse ( ) . unwrap ( ) ;
1330+ let mut http = StacksHttp :: new ( addr, & ConnectionOptions :: default ( ) ) ;
1331+
1332+ let request_data =
1333+ format ! ( "{verb} {path} HTTP/1.1\r \n Host: localhost:20443\r \n Connection: close\r \n \r \n " ) ;
1334+
1335+ let ( preamble, offset) = http. read_preamble ( request_data. as_bytes ( ) ) . unwrap ( ) ;
1336+ let result = http. read_payload ( & preamble, & request_data. as_bytes ( ) [ offset..] ) ;
1337+
1338+ match result {
1339+ // Valid request parsed successfully
1340+ Ok ( ( StacksHttpMessage :: Request ( _) , _) ) => {
1341+ assert_eq ! (
1342+ expected_status, 200 ,
1343+ "{verb} {path}: parsed OK but expected {expected_status}"
1344+ ) ;
1345+ }
1346+ // Server returned an error response (400, 404, 405, etc.)
1347+ Ok ( ( StacksHttpMessage :: Response ( resp) , _) )
1348+ | Ok ( ( StacksHttpMessage :: Error ( _, resp) , _) ) => {
1349+ assert_eq ! (
1350+ resp. preamble( ) . status_code,
1351+ expected_status,
1352+ "{verb} {path}: expected {expected_status}, got {}" ,
1353+ resp. preamble( ) . status_code
1354+ ) ;
1355+
1356+ // Verify Allow header is present for 405 responses
1357+ if let Some ( expected_method) = allow_contains {
1358+ let allow = resp. preamble ( ) . headers . get ( "allow" ) ;
1359+ assert ! ( allow. is_some( ) , "{verb} {path}: missing Allow header" ) ;
1360+ assert ! ( allow. unwrap( ) . contains( expected_method) ) ;
1361+ }
1362+ }
1363+ Err ( e) => panic ! ( "{verb} {path}: unexpected error {e:?}" ) ,
1364+ }
1365+ }
1366+ }
0 commit comments