diff --git a/docs/.agent/docs_coverage.md b/docs/.agent/docs_coverage.md index ba01c7ac..58508800 100644 --- a/docs/.agent/docs_coverage.md +++ b/docs/.agent/docs_coverage.md @@ -19,12 +19,23 @@ | Sync Validation | `docs/cookbook/src/crates/rustapi_validate.md` | `rustapi-validate/src/lib.rs` (`Validate`) | OK | | Async Validation | `docs/cookbook/src/crates/rustapi_validate.md` | `rustapi-validate/src/v2/mod.rs` (`AsyncValidate`) | OK | | **Extras** | | | | -| JWT Auth | `docs/cookbook/src/recipes/jwt_auth.md` | `rustapi-extras/src/jwt.rs` (`JwtLayer`) | OK | -| OAuth2 | `docs/cookbook/src/recipes/oauth2_client.md` | `rustapi-extras/src/oauth2.rs` (`OAuth2Client`) | OK | -| Database | `docs/cookbook/src/recipes/db_integration.md` | N/A (Integration pattern) | Needs Update | -| **Ecosystem** | | | | -| WebSockets | `docs/cookbook/src/recipes/websockets.md` | `rustapi-ws/src/lib.rs` (`WebSocketUpgrade`) | OK | -| SSR (View) | `docs/cookbook/src/recipes/server_side_rendering.md` | `rustapi-view/src/lib.rs` (`View`) | OK | -| gRPC | `docs/cookbook/src/recipes/grpc_integration.md` | `rustapi-grpc/src/lib.rs` (`TonicServer`) | OK | -| Jobs | `docs/cookbook/src/recipes/background_jobs.md` | `rustapi-jobs/src/lib.rs` (`Job`) | OK | -| TOON (AI) | `docs/cookbook/src/recipes/ai_integration.md` | `rustapi-toon/src/lib.rs` (`LlmResponse`) | OK | +| Auth (JWT) | `recipes/jwt_auth.md` | `rustapi-extras/src/jwt` | OK | +| Auth (OAuth2) | `recipes/oauth2_client.md` | `rustapi-extras/src/oauth2` | OK | +| Security | `recipes/csrf_protection.md` | `rustapi-extras/src/security` | OK | +| Observability | `crates/rustapi_extras.md` | `rustapi-extras/src/telemetry` | OK | +| Audit Logging | `recipes/audit_logging.md` | `rustapi-extras/src/audit` | OK | +| Middleware (Advanced) | `recipes/advanced_middleware.md` | `rustapi-extras/src/{rate_limit, dedup, cache}` | OK | +| **Jobs** | | | | +| Job Queue (Crate) | `crates/rustapi_jobs.md` | `rustapi-jobs` | OK | +| Background Jobs (Recipe) | `recipes/background_jobs.md` | `rustapi-jobs` | OK | +| **Integrations** | | | | +| gRPC | `recipes/grpc_integration.md` | `rustapi-grpc` | OK | +| SSR | `recipes/server_side_rendering.md` | `rustapi-view` | OK | +| AI / TOON | `recipes/ai_integration.md` | `rustapi-toon` | OK | +| WebSockets | `recipes/websockets.md` | `rustapi-ws` | Updated | +| **Learning** | | | | +| Structured Path | `learning/curriculum.md` | N/A | Updated (Mini Projects) | +| **Recipes** | | | | +| File Uploads | `recipes/file_uploads.md` | `rustapi-core` | Updated (Buffered) | +| Deployment | `recipes/deployment.md` | `cargo-rustapi` | OK | +| Testing | `recipes/testing.md` | `rustapi-testing` | OK | diff --git a/docs/.agent/docs_inventory.md b/docs/.agent/docs_inventory.md index 43b91771..b5ade764 100644 --- a/docs/.agent/docs_inventory.md +++ b/docs/.agent/docs_inventory.md @@ -1,25 +1,16 @@ # Documentation Inventory -| File Path | Purpose | Last Updated Version | Owner Crate | Status | -|-----------|---------|----------------------|-------------|--------| -| `docs/README.md` | Main entry point | 0.1.335 | rustapi-rs | OK | -| `docs/GETTING_STARTED.md` | Quick start guide | 0.1.335 | rustapi-rs | OK | -| `docs/ARCHITECTURE.md` | High-level architecture | 0.1.335 | rustapi-core | OK | -| `docs/FEATURES.md` | Feature list | 0.1.335 | rustapi-rs | OK | -| `docs/PHILOSOPHY.md` | Design philosophy | 0.1.335 | rustapi-rs | OK | -| `docs/native_openapi.md` | Native OpenAPI details | 0.1.335 | rustapi-openapi | OK | -| `docs/cookbook/src/SUMMARY.md` | Cookbook ToC | 0.1.335 | rustapi-rs | Needs Update | -| `docs/cookbook/src/introduction.md` | Cookbook Intro | 0.1.335 | rustapi-rs | OK | -| `docs/cookbook/src/troubleshooting.md` | Common issues | 0.1.335 | rustapi-rs | OK | -| `docs/cookbook/src/learning/curriculum.md` | Learning Path | 0.1.335 | rustapi-rs | Needs Update | -| `docs/cookbook/src/recipes/db_integration.md` | Database recipe | 0.1.335 | rustapi-rs | Needs Update | -| `docs/cookbook/src/recipes/file_uploads.md` | File upload recipe | 0.1.335 | rustapi-core | OK | -| `docs/cookbook/src/recipes/compression.md` | Compression recipe | 0.1.335 | rustapi-core | OK | -| `docs/cookbook/src/recipes/openapi_refs.md` | OpenAPI Refs recipe | 0.1.335 | rustapi-openapi | OK | -| `docs/cookbook/src/recipes/http3_quic.md` | HTTP/3 recipe | 0.1.335 | rustapi-core | OK | -| `docs/cookbook/src/recipes/jwt_auth.md` | JWT Auth recipe | 0.1.335 | rustapi-extras | OK | -| `docs/cookbook/src/recipes/websockets.md` | WebSocket recipe | 0.1.335 | rustapi-ws | OK | -| `docs/cookbook/src/recipes/server_side_rendering.md` | SSR recipe | 0.1.335 | rustapi-view | OK | -| `docs/cookbook/src/recipes/grpc_integration.md` | gRPC recipe | 0.1.335 | rustapi-grpc | OK | -| `docs/cookbook/src/recipes/background_jobs.md` | Jobs recipe | 0.1.335 | rustapi-jobs | OK | -| `docs/cookbook/src/recipes/ai_integration.md` | AI/TOON recipe | 0.1.335 | rustapi-toon | OK | +| File | Purpose | Owner Crate | Status | +|------|---------|-------------|--------| +| `README.md` | Project overview, key features, quick start | Root | OK | +| `docs/README.md` | Documentation landing page | Docs | OK | +| `docs/cookbook/src/SUMMARY.md` | Cookbook navigation structure | Docs | OK | +| `docs/cookbook/src/learning/curriculum.md` | Structured learning path | Docs | Updated (Mini Projects) | +| `docs/cookbook/src/recipes/file_uploads.md` | Recipe for File Uploads | Docs | Updated (Buffered) | +| `docs/cookbook/src/recipes/websockets.md` | Recipe for Real-time Chat | Docs | Updated (Extractors) | +| `docs/cookbook/src/recipes/background_jobs.md` | Recipe for Background Jobs | Docs | OK | +| `docs/cookbook/src/recipes/tuning.md` | Performance Tuning | Docs | DELETED | +| `docs/cookbook/src/recipes/new_feature.md` | New Feature Guide | Docs | DELETED | +| `docs/cookbook/src/architecture/action_pattern.md` | Action Pattern Guide | Docs | DELETED | +| `crates/rustapi-core/src/hateoas.rs` | API Reference for HATEOAS | rustapi-core | OK | +| `crates/rustapi-core/src/extract.rs` | API Reference for Extractors | rustapi-core | OK | diff --git a/docs/.agent/last_run.json b/docs/.agent/last_run.json index d30e66cb..b2bb51d1 100644 --- a/docs/.agent/last_run.json +++ b/docs/.agent/last_run.json @@ -1,5 +1,5 @@ { "last_processed_ref": "v0.1.335", - "date": "2026-02-17", - "notes": "Added recipes for Compression, OpenAPI Refs, File Uploads, and Database Integration. Updated Learning Path with new modules." + "date": "2026-02-19", + "notes": "Deleted incorrect recipes (tuning, action pattern). Updated File Uploads and WebSockets recipes for accuracy. Expanded Learning Path with mini projects." } diff --git a/docs/.agent/run_report_2026-02-19.md b/docs/.agent/run_report_2026-02-19.md new file mode 100644 index 00000000..7a1a0154 --- /dev/null +++ b/docs/.agent/run_report_2026-02-19.md @@ -0,0 +1,32 @@ +# Run Report: 2026-02-19 + +## Summary +Performed a continuous improvement pass on the documentation, focusing on accuracy in recipes and expanding the learning path. + +## Version Detection +- **Version**: 0.1.335 +- **Status**: No new version detected. Maintenance mode. + +## Changes +### 🗑️ Deleted Orphaned/Incorrect Files +- `docs/cookbook/src/recipes/tuning.md`: Referenced non-existent benchmark scripts. +- `docs/cookbook/src/recipes/new_feature.md`: Described non-existent "Action Pattern". +- `docs/cookbook/src/architecture/action_pattern.md`: Described non-existent "Action Pattern". + +### 📝 Updated Recipes +- **File Uploads** (`docs/cookbook/src/recipes/file_uploads.md`): + - Removed incorrect claims about streaming support in `Multipart`. + - Updated example to correctly use buffered `field.bytes()` and `field.save_to()`. + - Added warning about memory usage and `DefaultBodyLimit`. +- **WebSockets** (`docs/cookbook/src/recipes/websockets.md`): + - Corrected `ws_handler` signature to use `WebSocket` extractor instead of `WebSocketUpgrade`. + - Corrected `handle_socket` signature to accept `WebSocketStream`. + - Fixed imports and usage of `StreamExt`. + +### 📚 Learning Path +- **Curriculum** (`docs/cookbook/src/learning/curriculum.md`): + - Added "Mini Projects" to Module 1 (Echo Server), Module 2 (Calculator), and Module 3 (User Registry) to encourage hands-on practice. + +## TODOs +- Verify if `rustapi-core` plans to support streaming multipart in the future. +- Review other recipes for similar inaccuracies. diff --git a/docs/cookbook/src/architecture/action_pattern.md b/docs/cookbook/src/architecture/action_pattern.md deleted file mode 100644 index 3f4d9826..00000000 --- a/docs/cookbook/src/architecture/action_pattern.md +++ /dev/null @@ -1,39 +0,0 @@ -# The Action Pattern - -The **Action Pattern** is the central design abstraction of RustAPI. It redefines how we structure business logic, moving away from monolithic controllers/services into distinct, atomic units of work. - -## What is an Action? -An "Action" corresponds to exactly one business intent. -- **Good**: `CreateUser`, `DebitAccount`, `SendWelcomeEmail` -- **Bad**: `UserService` (too broad), `ManageOrders` (vague) - -## Why? -1. **Isolation**: Since every action is its own struct, it has its own file, its own tests, and its own unique set of dependencies. Modifying `CreateUser` cannot accidentally break `DeleteUser`. -2. **Testability**: You can inject mocks for just the dependencies this specific action needs. -3. **Readability**: The codebase becomes a catalog of capabilities. - -## Implementation Standard - -Every action implements a trait (usually `Runnable` or similar) that defines its contract. - -```rust -// Example Structure -pub struct CreateUser { - pub name: String, - pub email: String, -} - -impl Action for CreateUser { - type Output = User; - type Error = ApiError; - - async fn run(&self, ctx: &Context) -> Result { - // 1. Validate - // 2. Persist - // 3. Notify - } -} -``` - -> [!TIP] -> Use the `rustapi-macros` crate to auto-implement boilerplate for standard Actions. diff --git a/docs/cookbook/src/learning/curriculum.md b/docs/cookbook/src/learning/curriculum.md index dd20968c..9cae23dc 100644 --- a/docs/cookbook/src/learning/curriculum.md +++ b/docs/cookbook/src/learning/curriculum.md @@ -13,6 +13,9 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u - **Expected Output:** A running server that responds to `GET /` with "Hello World". - **Pitfalls:** Not enabling `tokio` features if setting up manually. +#### 🛠️ Mini Project: "The Echo Server" +Create a new endpoint `POST /echo` that accepts any text body and returns it back to the client. This verifies your setup handles basic I/O correctly. + #### 🧠 Knowledge Check 1. What command scaffolds a new RustAPI project? 2. Which feature flag is required for the async runtime? @@ -25,6 +28,9 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u - **Expected Output:** Endpoints that return static JSON data. - **Pitfalls:** Forgetting to register routes in `main.rs` if not using auto-discovery. +#### 🛠️ Mini Project: "The Calculator" +Create an endpoint `GET /add?a=5&b=10` that returns `{"result": 15}`. This practices query parameter extraction and JSON responses. + #### 🧠 Knowledge Check 1. Which macro is used to define a GET handler? 2. How do you return a JSON response from a handler? @@ -37,6 +43,9 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u - **Expected Output:** `GET /users/{id}` returns the ID. `POST /users` echoes the JSON body. - **Pitfalls:** Consuming the body twice (e.g., using `Json` and `Body` in the same handler). +#### 🛠️ Mini Project: "The User Registry" +Create a `POST /register` endpoint that accepts a JSON body `{"username": "...", "age": ...}` and returns a welcome message using the username. Use the `Json` extractor. + #### 🧠 Knowledge Check 1. Which extractor is used for URL parameters like `/users/:id`? 2. Which extractor parses the request body as JSON? diff --git a/docs/cookbook/src/recipes/file_uploads.md b/docs/cookbook/src/recipes/file_uploads.md index 6fd87fbc..bb5d67c2 100644 --- a/docs/cookbook/src/recipes/file_uploads.md +++ b/docs/cookbook/src/recipes/file_uploads.md @@ -1,6 +1,6 @@ # File Uploads -Handling file uploads efficiently is crucial for modern applications. RustAPI provides a `Multipart` extractor that allows you to stream uploads, enabling you to handle large files (e.g., 1GB+) without consuming proportional RAM. +Handling file uploads is a common requirement. RustAPI provides a `Multipart` extractor to parse `multipart/form-data` requests. ## Dependencies @@ -13,15 +13,13 @@ tokio = { version = "1", features = ["fs", "io-util"] } uuid = { version = "1", features = ["v4"] } ``` -## Streaming Upload Example +## Buffered Upload Example -Here is a complete, runnable example of a file upload server that streams files to a `./uploads` directory. +RustAPI's `Multipart` extractor currently buffers the entire request body into memory before parsing. This means it is suitable for small to medium file uploads (e.g., images, documents) but care must be taken with very large files to avoid running out of RAM. ```rust use rustapi_rs::prelude::*; -use rustapi_core::multipart::Multipart; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; +use rustapi_rs::extract::{Multipart, DefaultBodyLimit}; use std::path::Path; #[tokio::main] @@ -35,6 +33,10 @@ async fn main() -> Result<(), Box> { // Increase body limit to 1GB (default is usually 1MB) .body_limit(1024 * 1024 * 1024) .route("/upload", post(upload_handler)) + // Increase body limit to 50MB (default is usually 2MB) + // ⚠️ IMPORTANT: Since Multipart buffers the whole body, + // setting this too high can exhaust server memory. + .layer(DefaultBodyLimit::max(50 * 1024 * 1024)) .run("127.0.0.1:8080") .await } @@ -56,8 +58,13 @@ async fn upload_handler(mut multipart: Multipart) -> Result let mut uploaded_files = Vec::new(); // Iterate over the fields in the multipart form - while let Some(mut field) = multipart.next_field().await.map_err(|_| ApiError::bad_request("Invalid multipart"))? { + while let Some(field) = multipart.next_field().await.map_err(|_| ApiError::bad_request("Invalid multipart"))? { + // Skip fields that are not files + if !field.is_file() { + continue; + } + let file_name = field.file_name().unwrap_or("unknown.bin").to_string(); let content_type = field.content_type().unwrap_or("application/octet-stream").to_string(); @@ -65,18 +72,17 @@ async fn upload_handler(mut multipart: Multipart) -> Result // It could contain paths like "../../../etc/passwd". // Always generate a safe filename or sanitize inputs. let safe_filename = format!("{}-{}", uuid::Uuid::new_v4(), file_name); - let path = Path::new("./uploads").join(&safe_filename); - println!("Streaming file: {} -> {:?}", file_name, path); + // Option 1: Use the helper method (sanitizes filename automatically) + // field.save_to("./uploads", Some(&safe_filename)).await.map_err(|e| ApiError::internal(e.to_string()))?; - // Open destination file - let mut file = File::create(&path).await.map_err(|e| ApiError::internal(e.to_string()))?; + // Option 2: Manual write (gives you full control) + let data = field.bytes().await.map_err(|e| ApiError::internal(e.to_string()))?; + let path = Path::new("./uploads").join(&safe_filename); - // Stream the field content chunk-by-chunk - // This is memory efficient even for large files. - while let Some(chunk) = field.chunk().await.map_err(|_| ApiError::bad_request("Stream error"))? { - file.write_all(&chunk).await.map_err(|e| ApiError::internal(e.to_string()))?; - } + tokio::fs::write(&path, &data).await.map_err(|e| ApiError::internal(e.to_string()))?; + + println!("Saved file: {} -> {:?}", file_name, path); uploaded_files.push(FileResult { original_name: file_name, @@ -94,13 +100,14 @@ async fn upload_handler(mut multipart: Multipart) -> Result ## Key Concepts -### 1. Streaming vs Buffering -By default, some frameworks load the entire file into RAM. RustAPI's `Multipart` allows you to process the stream incrementally using `field.chunk()`. -- **Buffering**: `field.bytes().await` (Load all into RAM - simple but dangerous for large files) -- **Streaming**: `field.chunk().await` (Load small chunks - scalable) +### 1. Buffering +RustAPI loads the entire `multipart/form-data` body into memory. +- **Pros**: Simple API, easy to work with. +- **Cons**: High memory usage for concurrent large uploads. +- **Mitigation**: Set a reasonable `DefaultBodyLimit` (e.g., 10MB - 100MB) to prevent DoS attacks. ### 2. Body Limits -The default request body limit is often small (e.g., 1MB) to prevent DoS attacks. You must explicitly increase this limit for file upload routes using `RustApi::new().body_limit(size)`. This applies globally to the application instance. If you need different limits for different routes, consider creating separate router instances or using a custom layer. +The default request body limit is small (2MB) to prevent attacks. You **must** explicitly increase this limit for file upload routes using `.layer(DefaultBodyLimit::max(size_in_bytes))`. ### 3. Security - **Path Traversal**: Malicious users can send filenames like `../../system32/cmd.exe`. Always rename files or sanitize filenames strictly. diff --git a/docs/cookbook/src/recipes/new_feature.md b/docs/cookbook/src/recipes/new_feature.md deleted file mode 100644 index 27199304..00000000 --- a/docs/cookbook/src/recipes/new_feature.md +++ /dev/null @@ -1,73 +0,0 @@ -# Recipe: Adding a New Feature - -This guide walks you through the standard process of adding a new feature to RustAPI using the Action pattern. - -## Prerequisites -- [x] You have the repo cloned. -- [x] You understand the [Action Pattern](../architecture/action_pattern.md). - -## Step 1: Define the Action Struct -Create a new file in the appropriate crate (or `crates/rustapi-core/src/actions/` if it's core). - -```rust -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize)] -pub struct CreateWidget { - pub name: String, - pub quantity: i32, -} -``` - -## Step 2: Implement the Logic -Implement the logic trait. - -```rust -impl Action for CreateWidget { - type Output = Widget; - type Error = ApiError; - - async fn run(&self, ctx: &Context) -> Result { - // Validation - if self.quantity < 0 { - return Err(ApiError::BadRequest("Quantity must be positive")); - } - - // Database - let widget = sqlx::query_as!( - Widget, - "INSERT INTO widgets (name, quantity) VALUES ($1, $2) RETURNING *", - self.name, - self.quantity - ) - .fetch_one(&ctx.db) - .await?; - - Ok(widget) - } -} -``` - -## Step 3: Register the Route -Add the action to your router configuration. - -```rust -// in router.rs or module definition -.route("/widgets", post(handle_action::)) -``` - -## Step 4: Add Tests -Create a dedicated test file or use the `tests` module in the same file. - -```rust -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validation() { - let action = CreateWidget { name: "Test".into(), quantity: -1 }; - // Assert it returns error... - } -} -``` diff --git a/docs/cookbook/src/recipes/tuning.md b/docs/cookbook/src/recipes/tuning.md deleted file mode 100644 index b834460e..00000000 --- a/docs/cookbook/src/recipes/tuning.md +++ /dev/null @@ -1,31 +0,0 @@ -# Recipe: Performance Tuning - -When the API feels slow, don't guess—profile and benchmark. - -## 1. Run the Suite -Use the integrated benchmark tool. - -```powershell -.\benches\run_benchmarks.ps1 -``` - -This runs: -1. **Micro-benchmarks** (internal `cargo bench` via Criterion). -2. **Macro-benchmarks** (external `hey` HTTP load test). - -## 2. Interpret the Data -- **High Latency, Low CPU**: You are IO-bound. Check database queries (indexes?) or external API calls. -- **High Latency, High CPU**: You are CPU-bound. - - Are you doing heavy JSON serialization? - - Are you cloning Strings unnecessarily? - - Are you blocking the async runtime? - -## 3. Common Optimizations -- **Allocations**: Use `cow` (Clone-on-Write) or `&str` reference passing. -- **JSON**: Ensure `serde_json` is not re-parsing the same data. -- **Database**: Use connection pooling correctly (already configured in Core). - -## 4. Verify -After making a change, run the benchmark script again. -- Did `Requests/sec` go up? -- Did `Average Latency` go down? diff --git a/docs/cookbook/src/recipes/websockets.md b/docs/cookbook/src/recipes/websockets.md index 653b0a87..d6daf493 100644 --- a/docs/cookbook/src/recipes/websockets.md +++ b/docs/cookbook/src/recipes/websockets.md @@ -7,19 +7,20 @@ WebSockets allow full-duplex communication between the client and server. RustAP ```toml [dependencies] rustapi-ws = "0.1.335" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["sync", "macros"] } futures = "0.3" ``` ## The Upgrade Handler -WebSocket connections start as HTTP requests. We "upgrade" them. +WebSocket connections start as HTTP requests. We "upgrade" them using the `WebSocket` extractor. ```rust -use rustapi_ws::{WebSocket, WebSocketUpgrade, Message}; +use rustapi_ws::{WebSocket, WebSocketStream, Message}; use rustapi_rs::prelude::*; use std::sync::Arc; use tokio::sync::broadcast; +use futures::stream::StreamExt; // Shared state for broadcasting messages to all connected clients pub struct AppState { @@ -27,7 +28,7 @@ pub struct AppState { } async fn ws_handler( - ws: WebSocketUpgrade, + ws: WebSocket, State(state): State>, ) -> impl IntoResponse { // Finalize the upgrade and spawn the socket handler @@ -38,9 +39,7 @@ async fn ws_handler( ## Handling the Connection ```rust -use futures::{sink::SinkExt, stream::StreamExt}; - -async fn handle_socket(socket: WebSocket, state: Arc) { +async fn handle_socket(socket: WebSocketStream, state: Arc) { // Split the socket into a sender and receiver let (mut sender, mut receiver) = socket.split(); @@ -51,7 +50,7 @@ async fn handle_socket(socket: WebSocket, state: Arc) { let mut send_task = tokio::spawn(async move { while let Ok(msg) = rx.recv().await { // If the client disconnects, this will fail and we break - if sender.send(Message::Text(msg)).await.is_err() { + if sender.send(Message::text(msg)).await.is_err() { break; } } @@ -60,10 +59,14 @@ async fn handle_socket(socket: WebSocket, state: Arc) { // Handle incoming messages from THIS client let mut recv_task = tokio::spawn(async move { while let Some(Ok(msg)) = receiver.next().await { - if let Message::Text(text) = msg { - println!("Received message: {}", text); - // Broadcast it to everyone else - let _ = state.tx.send(format!("User says: {}", text)); + match msg { + Message::Text(text) => { + println!("Received message: {}", text); + // Broadcast it to everyone else + let _ = state.tx.send(format!("User says: {}", text)); + } + Message::Close(_) => break, + _ => {} } } }); @@ -95,7 +98,7 @@ async fn main() { ## Client-Side Testing -You can simpler use JavaScript in the browser console: +You can simply use JavaScript in the browser console: ```javascript let ws = new WebSocket("ws://localhost:3000/ws"); @@ -104,11 +107,11 @@ ws.onmessage = (event) => { console.log("Message from server:", event.data); }; -ws.send("Hello form JS!"); +ws.send("Hello from JS!"); ``` ## Advanced Patterns -1. **User Authentication**: Use the same `AuthUser` extractor in the `ws_handler`. If authentication fails, return an error *before* upgrading. +1. **User Authentication**: Use the same `AuthUser` extractor in the `ws_handler`. If authentication fails, return an error *before* calling `ws.on_upgrade`. 2. **Ping/Pong**: Browsers and Load Balancers kill idle connections. Implement a heartbeat mechanism to keep the connection alive. - `rustapi-ws` handles low-level ping/pong frames automatically in many cases, but application-level pings are also robust.