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 58 ] 59 59 60 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]] 61 73 name = "actix-http" 62 74 version = "3.11.2" 63 75 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 254 266 ] 255 267 256 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]] 257 275 name = "anstream" 258 276 version = "0.6.21" 259 277 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 448 466 ] 449 467 450 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]] 451 475 name = "crypto-common" 452 476 version = "0.1.6" 453 477 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 458 482 ] 459 483 460 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]] 461 499 name = "deranged" 462 500 version = "0.5.4" 463 501 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 574 612 dependencies = [ 575 613 "actix-cors", 576 614 "actix-files", 615 + "actix-governor", 577 616 "actix-web", 578 617 "anyhow", 579 618 "base64", ··· 640 679 ] 641 680 642 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]] 643 697 name = "futures-channel" 644 698 version = "0.3.31" 645 699 source = "registry+https://github.com/rust-lang/crates.io-index" 646 700 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 647 701 dependencies = [ 648 702 "futures-core", 703 + "futures-sink", 649 704 ] 650 705 651 706 [[package]] ··· 655 710 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 656 711 657 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]] 658 741 name = "futures-sink" 659 742 version = "0.3.31" 660 743 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 667 750 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 668 751 669 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]] 670 759 name = "futures-util" 671 760 version = "0.3.31" 672 761 source = "registry+https://github.com/rust-lang/crates.io-index" 673 762 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 674 763 dependencies = [ 764 + "futures-channel", 675 765 "futures-core", 766 + "futures-io", 767 + "futures-macro", 768 + "futures-sink", 676 769 "futures-task", 770 + "memchr", 677 771 "pin-project-lite", 678 772 "pin-utils", 679 773 "slab", ··· 707 801 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 708 802 dependencies = [ 709 803 "cfg-if", 804 + "js-sys", 710 805 "libc", 711 806 "r-efi", 712 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", 713 832 ] 714 833 715 834 [[package]] ··· 752 871 753 872 [[package]] 754 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" 755 891 version = "0.16.0" 756 892 source = "registry+https://github.com/rust-lang/crates.io-index" 757 893 checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" ··· 1019 1155 checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 1020 1156 dependencies = [ 1021 1157 "equivalent", 1022 - "hashbrown", 1158 + "hashbrown 0.16.0", 1023 1159 ] 1024 1160 1025 1161 [[package]] ··· 1210 1346 "security-framework-sys", 1211 1347 "tempfile", 1212 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" 1213 1355 1214 1356 [[package]] 1215 1357 name = "num-conv" ··· 1369 1511 ] 1370 1512 1371 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]] 1372 1529 name = "quote" 1373 1530 version = "1.0.41" 1374 1531 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1410 1567 checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1411 1568 dependencies = [ 1412 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", 1413 1579 ] 1414 1580 1415 1581 [[package]] ··· 1725 1891 dependencies = [ 1726 1892 "libc", 1727 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", 1728 1903 ] 1729 1904 1730 1905 [[package]] ··· 2186 2361 "js-sys", 2187 2362 "wasm-bindgen", 2188 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" 2189 2396 2190 2397 [[package]] 2191 2398 name = "windows-link"
+1
Cargo.toml
··· 17 17 env_logger = "0.11" 18 18 log = "0.4" 19 19 base64 = "0.22" 20 + actix-governor = "0.10.0"
+9
src/main.rs
··· 5 5 6 6 use actix_cors::Cors; 7 7 use actix_files as fs; 8 + use actix_governor::{Governor, GovernorConfigBuilder}; 8 9 use actix_web::{middleware, web, App, HttpResponse, HttpServer}; 9 10 use anyhow::Result; 10 11 use config::Config; ··· 26 27 27 28 log::info!("starting bufo search server on {}:{}", host, port); 28 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 + 29 37 HttpServer::new(move || { 30 38 let cors = Cors::permissive(); 31 39 ··· 36 44 .route("/", web::get().to(index)) 37 45 .service( 38 46 web::scope("/api") 47 + .wrap(Governor::new(&governor_conf)) 39 48 .route("/search", web::post().to(search::search)) 40 49 .route("/health", web::get().to(|| async { HttpResponse::Ok().body("ok") })) 41 50 )