semantic bufo search find-bufo.com
bufo

add rate limiting to protect API from abuse

- 10 requests per minute per IP (burst of 10)
- returns 429 Too Many Requests after limit exceeded
- protects voyage API costs from query spam

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+218 -1
+208 -1
Cargo.lock
··· 58 ] 59 60 [[package]] 61 name = "actix-http" 62 version = "3.11.2" 63 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 254 ] 255 256 [[package]] 257 name = "anstream" 258 version = "0.6.21" 259 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 448 ] 449 450 [[package]] 451 name = "crypto-common" 452 version = "0.1.6" 453 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 458 ] 459 460 [[package]] 461 name = "deranged" 462 version = "0.5.4" 463 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 574 dependencies = [ 575 "actix-cors", 576 "actix-files", 577 "actix-web", 578 "anyhow", 579 "base64", ··· 640 ] 641 642 [[package]] 643 name = "futures-channel" 644 version = "0.3.31" 645 source = "registry+https://github.com/rust-lang/crates.io-index" 646 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 647 dependencies = [ 648 "futures-core", 649 ] 650 651 [[package]] ··· 655 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 656 657 [[package]] 658 name = "futures-sink" 659 version = "0.3.31" 660 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 667 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 668 669 [[package]] 670 name = "futures-util" 671 version = "0.3.31" 672 source = "registry+https://github.com/rust-lang/crates.io-index" 673 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 674 dependencies = [ 675 "futures-core", 676 "futures-task", 677 "pin-project-lite", 678 "pin-utils", 679 "slab", ··· 707 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 708 dependencies = [ 709 "cfg-if", 710 "libc", 711 "r-efi", 712 "wasip2", 713 ] 714 715 [[package]] ··· 752 753 [[package]] 754 name = "hashbrown" 755 version = "0.16.0" 756 source = "registry+https://github.com/rust-lang/crates.io-index" 757 checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" ··· 1019 checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 1020 dependencies = [ 1021 "equivalent", 1022 - "hashbrown", 1023 ] 1024 1025 [[package]] ··· 1210 "security-framework-sys", 1211 "tempfile", 1212 ] 1213 1214 [[package]] 1215 name = "num-conv" ··· 1369 ] 1370 1371 [[package]] 1372 name = "quote" 1373 version = "1.0.41" 1374 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1410 checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1411 dependencies = [ 1412 "getrandom 0.3.4", 1413 ] 1414 1415 [[package]] ··· 1725 dependencies = [ 1726 "libc", 1727 "windows-sys 0.60.2", 1728 ] 1729 1730 [[package]] ··· 2186 "js-sys", 2187 "wasm-bindgen", 2188 ] 2189 2190 [[package]] 2191 name = "windows-link"
··· 58 ] 59 60 [[package]] 61 + name = "actix-governor" 62 + version = "0.10.0" 63 + source = "registry+https://github.com/rust-lang/crates.io-index" 64 + checksum = "6a7ffa43d3e1e92518355ffbc82c146b5f0fe24fba87f19f405270da7a7b3c1e" 65 + dependencies = [ 66 + "actix-http", 67 + "actix-web", 68 + "futures", 69 + "governor", 70 + ] 71 + 72 + [[package]] 73 name = "actix-http" 74 version = "3.11.2" 75 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 266 ] 267 268 [[package]] 269 + name = "allocator-api2" 270 + version = "0.2.21" 271 + source = "registry+https://github.com/rust-lang/crates.io-index" 272 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 273 + 274 + [[package]] 275 name = "anstream" 276 version = "0.6.21" 277 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 466 ] 467 468 [[package]] 469 + name = "crossbeam-utils" 470 + version = "0.8.21" 471 + source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 473 + 474 + [[package]] 475 name = "crypto-common" 476 version = "0.1.6" 477 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 482 ] 483 484 [[package]] 485 + name = "dashmap" 486 + version = "6.1.0" 487 + source = "registry+https://github.com/rust-lang/crates.io-index" 488 + checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 489 + dependencies = [ 490 + "cfg-if", 491 + "crossbeam-utils", 492 + "hashbrown 0.14.5", 493 + "lock_api", 494 + "once_cell", 495 + "parking_lot_core", 496 + ] 497 + 498 + [[package]] 499 name = "deranged" 500 version = "0.5.4" 501 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 612 dependencies = [ 613 "actix-cors", 614 "actix-files", 615 + "actix-governor", 616 "actix-web", 617 "anyhow", 618 "base64", ··· 679 ] 680 681 [[package]] 682 + name = "futures" 683 + version = "0.3.31" 684 + source = "registry+https://github.com/rust-lang/crates.io-index" 685 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 686 + dependencies = [ 687 + "futures-channel", 688 + "futures-core", 689 + "futures-executor", 690 + "futures-io", 691 + "futures-sink", 692 + "futures-task", 693 + "futures-util", 694 + ] 695 + 696 + [[package]] 697 name = "futures-channel" 698 version = "0.3.31" 699 source = "registry+https://github.com/rust-lang/crates.io-index" 700 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 701 dependencies = [ 702 "futures-core", 703 + "futures-sink", 704 ] 705 706 [[package]] ··· 710 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 711 712 [[package]] 713 + name = "futures-executor" 714 + version = "0.3.31" 715 + source = "registry+https://github.com/rust-lang/crates.io-index" 716 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 717 + dependencies = [ 718 + "futures-core", 719 + "futures-task", 720 + "futures-util", 721 + ] 722 + 723 + [[package]] 724 + name = "futures-io" 725 + version = "0.3.31" 726 + source = "registry+https://github.com/rust-lang/crates.io-index" 727 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 728 + 729 + [[package]] 730 + name = "futures-macro" 731 + version = "0.3.31" 732 + source = "registry+https://github.com/rust-lang/crates.io-index" 733 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 734 + dependencies = [ 735 + "proc-macro2", 736 + "quote", 737 + "syn", 738 + ] 739 + 740 + [[package]] 741 name = "futures-sink" 742 version = "0.3.31" 743 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 750 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 751 752 [[package]] 753 + name = "futures-timer" 754 + version = "3.0.3" 755 + source = "registry+https://github.com/rust-lang/crates.io-index" 756 + checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 757 + 758 + [[package]] 759 name = "futures-util" 760 version = "0.3.31" 761 source = "registry+https://github.com/rust-lang/crates.io-index" 762 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 763 dependencies = [ 764 + "futures-channel", 765 "futures-core", 766 + "futures-io", 767 + "futures-macro", 768 + "futures-sink", 769 "futures-task", 770 + "memchr", 771 "pin-project-lite", 772 "pin-utils", 773 "slab", ··· 801 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 802 dependencies = [ 803 "cfg-if", 804 + "js-sys", 805 "libc", 806 "r-efi", 807 "wasip2", 808 + "wasm-bindgen", 809 + ] 810 + 811 + [[package]] 812 + name = "governor" 813 + version = "0.10.1" 814 + source = "registry+https://github.com/rust-lang/crates.io-index" 815 + checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" 816 + dependencies = [ 817 + "cfg-if", 818 + "dashmap", 819 + "futures-sink", 820 + "futures-timer", 821 + "futures-util", 822 + "getrandom 0.3.4", 823 + "hashbrown 0.15.5", 824 + "nonzero_ext", 825 + "parking_lot", 826 + "portable-atomic", 827 + "quanta", 828 + "rand", 829 + "smallvec", 830 + "spinning_top", 831 + "web-time", 832 ] 833 834 [[package]] ··· 871 872 [[package]] 873 name = "hashbrown" 874 + version = "0.14.5" 875 + source = "registry+https://github.com/rust-lang/crates.io-index" 876 + checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 877 + 878 + [[package]] 879 + name = "hashbrown" 880 + version = "0.15.5" 881 + source = "registry+https://github.com/rust-lang/crates.io-index" 882 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 883 + dependencies = [ 884 + "allocator-api2", 885 + "equivalent", 886 + "foldhash", 887 + ] 888 + 889 + [[package]] 890 + name = "hashbrown" 891 version = "0.16.0" 892 source = "registry+https://github.com/rust-lang/crates.io-index" 893 checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" ··· 1155 checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 1156 dependencies = [ 1157 "equivalent", 1158 + "hashbrown 0.16.0", 1159 ] 1160 1161 [[package]] ··· 1346 "security-framework-sys", 1347 "tempfile", 1348 ] 1349 + 1350 + [[package]] 1351 + name = "nonzero_ext" 1352 + version = "0.3.0" 1353 + source = "registry+https://github.com/rust-lang/crates.io-index" 1354 + checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" 1355 1356 [[package]] 1357 name = "num-conv" ··· 1511 ] 1512 1513 [[package]] 1514 + name = "quanta" 1515 + version = "0.12.6" 1516 + source = "registry+https://github.com/rust-lang/crates.io-index" 1517 + checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" 1518 + dependencies = [ 1519 + "crossbeam-utils", 1520 + "libc", 1521 + "once_cell", 1522 + "raw-cpuid", 1523 + "wasi", 1524 + "web-sys", 1525 + "winapi", 1526 + ] 1527 + 1528 + [[package]] 1529 name = "quote" 1530 version = "1.0.41" 1531 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1567 checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1568 dependencies = [ 1569 "getrandom 0.3.4", 1570 + ] 1571 + 1572 + [[package]] 1573 + name = "raw-cpuid" 1574 + version = "11.6.0" 1575 + source = "registry+https://github.com/rust-lang/crates.io-index" 1576 + checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" 1577 + dependencies = [ 1578 + "bitflags", 1579 ] 1580 1581 [[package]] ··· 1891 dependencies = [ 1892 "libc", 1893 "windows-sys 0.60.2", 1894 + ] 1895 + 1896 + [[package]] 1897 + name = "spinning_top" 1898 + version = "0.3.0" 1899 + source = "registry+https://github.com/rust-lang/crates.io-index" 1900 + checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" 1901 + dependencies = [ 1902 + "lock_api", 1903 ] 1904 1905 [[package]] ··· 2361 "js-sys", 2362 "wasm-bindgen", 2363 ] 2364 + 2365 + [[package]] 2366 + name = "web-time" 2367 + version = "1.1.0" 2368 + source = "registry+https://github.com/rust-lang/crates.io-index" 2369 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2370 + dependencies = [ 2371 + "js-sys", 2372 + "wasm-bindgen", 2373 + ] 2374 + 2375 + [[package]] 2376 + name = "winapi" 2377 + version = "0.3.9" 2378 + source = "registry+https://github.com/rust-lang/crates.io-index" 2379 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2380 + dependencies = [ 2381 + "winapi-i686-pc-windows-gnu", 2382 + "winapi-x86_64-pc-windows-gnu", 2383 + ] 2384 + 2385 + [[package]] 2386 + name = "winapi-i686-pc-windows-gnu" 2387 + version = "0.4.0" 2388 + source = "registry+https://github.com/rust-lang/crates.io-index" 2389 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2390 + 2391 + [[package]] 2392 + name = "winapi-x86_64-pc-windows-gnu" 2393 + version = "0.4.0" 2394 + source = "registry+https://github.com/rust-lang/crates.io-index" 2395 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2396 2397 [[package]] 2398 name = "windows-link"
+1
Cargo.toml
··· 17 env_logger = "0.11" 18 log = "0.4" 19 base64 = "0.22"
··· 17 env_logger = "0.11" 18 log = "0.4" 19 base64 = "0.22" 20 + actix-governor = "0.10.0"
+9
src/main.rs
··· 5 6 use actix_cors::Cors; 7 use actix_files as fs; 8 use actix_web::{middleware, web, App, HttpResponse, HttpServer}; 9 use anyhow::Result; 10 use config::Config; ··· 26 27 log::info!("starting bufo search server on {}:{}", host, port); 28 29 HttpServer::new(move || { 30 let cors = Cors::permissive(); 31 ··· 36 .route("/", web::get().to(index)) 37 .service( 38 web::scope("/api") 39 .route("/search", web::post().to(search::search)) 40 .route("/health", web::get().to(|| async { HttpResponse::Ok().body("ok") })) 41 )
··· 5 6 use actix_cors::Cors; 7 use actix_files as fs; 8 + use actix_governor::{Governor, GovernorConfigBuilder}; 9 use actix_web::{middleware, web, App, HttpResponse, HttpServer}; 10 use anyhow::Result; 11 use config::Config; ··· 27 28 log::info!("starting bufo search server on {}:{}", host, port); 29 30 + // rate limiter: 10 requests per minute per IP 31 + let governor_conf = GovernorConfigBuilder::default() 32 + .milliseconds_per_request(6000) // 1 request per 6 seconds = 10 per minute 33 + .burst_size(10) 34 + .finish() 35 + .unwrap(); 36 + 37 HttpServer::new(move || { 38 let cors = Cors::permissive(); 39 ··· 44 .route("/", web::get().to(index)) 45 .service( 46 web::scope("/api") 47 + .wrap(Governor::new(&governor_conf)) 48 .route("/search", web::post().to(search::search)) 49 .route("/health", web::get().to(|| async { HttpResponse::Ok().body("ok") })) 50 )