Skip to content

Commit f268d40

Browse files
authored
Merge pull request #121 from Tuntii/docs/enterprise-and-testing-14429851771554642366
docs: Enterprise Learning Path & Testing Recipe
2 parents 1f97f43 + 287ab64 commit f268d40

6 files changed

Lines changed: 329 additions & 39 deletions

File tree

docs/.agent/last_run.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"last_processed_ref": "v0.1.335",
33
"date": "2025-02-24",
4-
"notes": "Added background jobs recipe and expanded learning path with Module 10."
4+
"notes": "Added Phase 4 (Enterprise Scale) to Learning Path, created Testing recipe, and updated File Uploads recipe."
55
}

docs/.agent/run_report_2025-02-24.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,37 @@ This run focuses on expanding the cookbook and refining the learning path to inc
3131
## 4. Open Questions / TODOs
3232
- Investigate adding `rustapi-jobs` as a re-export in `rustapi-rs` for better "batteries-included" experience in future versions.
3333
- Consider adding more backend examples (Redis, Postgres) to the cookbook recipe when environment setup allows.
34+
35+
---
36+
37+
# Docs Maintenance Run Report: 2025-02-24 (Run 2)
38+
39+
## 1. Version Detection
40+
- **Repo Version**: `v0.1.335` (Unchanged)
41+
- **Result**: Continuing with Continuous Improvement phase.
42+
43+
## 2. Changes Summary
44+
This run focuses on "Enterprise Scale" documentation, testing strategies, and improving existing recipes.
45+
46+
### New Content
47+
- **Cookbook Recipe**: `docs/cookbook/src/recipes/testing.md` - Comprehensive guide to `rustapi-testing`, `TestClient`, and `MockServer`.
48+
- **Learning Path Phase**: Added "Phase 4: Enterprise Scale" to `docs/cookbook/src/learning/curriculum.md`, covering Observability, Resilience, and High Performance.
49+
50+
### Updates
51+
- **File Uploads Recipe**: Rewrote `docs/cookbook/src/recipes/file_uploads.md` with a complete, runnable example using `Multipart` streaming and improved security guidance.
52+
- **Cookbook Summary**: Added "Testing & Mocking" to `docs/cookbook/src/SUMMARY.md`.
53+
54+
## 3. Improvement Details
55+
- **Learning Path**:
56+
- Added Modules 11 (Observability), 12 (Resilience & Security), 13 (High Performance).
57+
- Added "Phase 4 Capstone: The High-Scale Event Platform".
58+
- **Testing Recipe**:
59+
- Detailed usage of `TestClient` for integration tests.
60+
- Example of mocking external services with `MockServer`.
61+
- **File Uploads**:
62+
- Replaced partial snippets with a full `main.rs` style example.
63+
- Clarified streaming vs buffering and added security warnings.
64+
65+
## 4. Open Questions / TODOs
66+
- **Status Page**: `recipes/status_page.md` exists but might need more visibility in the Learning Path (maybe in Module 11?).
67+
- **Observability**: A dedicated recipe for OpenTelemetry setup would be beneficial (currently covered in crate docs).

docs/cookbook/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
- [JWT Authentication](recipes/jwt_auth.md)
3434
- [CSRF Protection](recipes/csrf_protection.md)
3535
- [Database Integration](recipes/db_integration.md)
36+
- [Testing & Mocking](recipes/testing.md)
3637
- [File Uploads](recipes/file_uploads.md)
3738
- [Background Jobs](recipes/background_jobs.md)
3839
- [Custom Middleware](recipes/custom_middleware.md)

docs/cookbook/src/learning/curriculum.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,67 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u
170170

171171
---
172172

173+
## Phase 4: Enterprise Scale
174+
175+
**Goal:** Build observable, resilient, and high-performance distributed systems.
176+
177+
### Module 11: Observability
178+
- **Prerequisites:** Phase 3.
179+
- **Reading:** [Observability (Extras)](../crates/rustapi_extras.md#observability), [Structured Logging](../crates/rustapi_extras.md#structured-logging).
180+
- **Task:**
181+
1. Enable `structured-logging` and `otel` features.
182+
2. Configure tracing to export spans to Jaeger (or console for dev).
183+
3. Add custom metrics for "active_users" and "jobs_processed".
184+
- **Expected Output:** Logs are JSON formatted with trace IDs. Metrics endpoint exposes Prometheus data.
185+
- **Pitfalls:** High cardinality in metric labels (e.g., using user IDs as labels).
186+
187+
#### 🧠 Knowledge Check
188+
1. What is the difference between logging and tracing?
189+
2. How do you correlate logs across microservices?
190+
3. What is the standard format for structured logs in RustAPI?
191+
192+
### Module 12: Resilience & Security
193+
- **Prerequisites:** Phase 3.
194+
- **Reading:** [Resilience Patterns](../recipes/resilience.md), [Time-Travel Debugging](../recipes/replay.md).
195+
- **Task:**
196+
1. Wrap an external API call with a `CircuitBreaker`.
197+
2. Implement `RetryLayer` for transient failures.
198+
3. (Optional) Use `ReplayLayer` to record and replay a tricky bug scenario.
199+
- **Expected Output:** System degrades gracefully when external service is down. Replay file captures the exact request sequence.
200+
- **Pitfalls:** Infinite retry loops or retrying non-idempotent operations.
201+
202+
#### 🧠 Knowledge Check
203+
1. What state does a Circuit Breaker have when it stops traffic?
204+
2. Why is jitter important in retry strategies?
205+
3. How does Time-Travel Debugging help with "Heisenbugs"?
206+
207+
### Module 13: High Performance
208+
- **Prerequisites:** Phase 3.
209+
- **Reading:** [HTTP/3 (QUIC)](../recipes/http3_quic.md), [Performance Tuning](../recipes/high_performance.md).
210+
- **Task:**
211+
1. Enable `http3` feature and generate self-signed certs.
212+
2. Serve traffic over QUIC.
213+
3. Implement response caching for a heavy computation endpoint.
214+
- **Expected Output:** Browser/Client connects via HTTP/3. Repeated requests are served instantly from cache.
215+
- **Pitfalls:** Caching private user data without proper keys.
216+
217+
#### 🧠 Knowledge Check
218+
1. What transport protocol does HTTP/3 use?
219+
2. How does `simd-json` improve performance?
220+
3. When should you *not* use caching?
221+
222+
### 🏆 Phase 4 Capstone: "The High-Scale Event Platform"
223+
**Objective:** Architect a system capable of handling thousands of events per second.
224+
**Requirements:**
225+
- **Ingestion:** HTTP/3 endpoint receiving JSON events.
226+
- **Processing:** Push events to a `rustapi-jobs` queue (Redis backend).
227+
- **Storage:** Workers process events and store aggregates in a database.
228+
- **Observability:** Full tracing from ingestion to storage.
229+
- **Resilience:** Circuit breakers on database writes.
230+
- **Testing:** Load test the ingestion endpoint (e.g., with k6 or similar) and observe metrics.
231+
232+
---
233+
173234
## Next Steps
174235

175236
* Explore the [Examples Repository](https://github.com/Tuntii/rustapi-rs-examples).
Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,133 @@
11
# File Uploads
22

3-
Handling file uploads efficiently is crucial. RustAPI allows you to stream `Multipart` data, meaning you can handle 1GB uploads without using 1GB of RAM.
3+
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.
44

55
## Dependencies
66

7+
Add `uuid` and `tokio` with `fs` features to your `Cargo.toml`.
8+
79
```toml
810
[dependencies]
911
rustapi-rs = "0.1.335"
1012
tokio = { version = "1", features = ["fs", "io-util"] }
1113
uuid = { version = "1", features = ["v4"] }
1214
```
1315

14-
## Streaming Upload Handler
16+
## Streaming Upload Example
1517

16-
This handler reads the incoming stream part-by-part and writes it directly to disk (or S3).
18+
Here is a complete, runnable example of a file upload server that streams files to a `./uploads` directory.
1719

1820
```rust
1921
use rustapi_rs::prelude::*;
20-
use rustapi_rs::extract::Multipart;
22+
use rustapi_rs::extract::{Multipart, DefaultBodyLimit};
2123
use tokio::fs::File;
2224
use tokio::io::AsyncWriteExt;
25+
use std::path::Path;
26+
27+
#[tokio::main]
28+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
29+
// Ensure uploads directory exists
30+
tokio::fs::create_dir_all("./uploads").await?;
31+
32+
println!("Starting Upload Server at http://127.0.0.1:8080");
33+
34+
RustApi::new()
35+
.route("/upload", post(upload_handler))
36+
// Increase body limit to 1GB (default is usually 2MB)
37+
.layer(DefaultBodyLimit::max(1024 * 1024 * 1024))
38+
.run("127.0.0.1:8080")
39+
.await
40+
}
2341

24-
async fn upload_file(mut multipart: Multipart) -> Result<StatusCode, ApiError> {
25-
// Iterate over the fields
26-
while let Some(field) = multipart.next_field().await.map_err(|_| ApiError::BadRequest)? {
42+
async fn upload_handler(mut multipart: Multipart) -> Result<Json<UploadResponse>> {
43+
let mut uploaded_files = Vec::new();
44+
45+
// Iterate over the fields in the multipart form
46+
while let Some(mut field) = multipart.next_field().await.map_err(|_| ApiError::bad_request("Invalid multipart"))? {
2747

28-
let name = field.name().unwrap_or("file").to_string();
2948
let file_name = field.file_name().unwrap_or("unknown.bin").to_string();
3049
let content_type = field.content_type().unwrap_or("application/octet-stream").to_string();
3150

32-
println!("Uploading: {} ({})", file_name, content_type);
51+
// ⚠️ Security: Never trust the user-provided filename directly!
52+
// It could contain paths like "../../../etc/passwd".
53+
// Always generate a safe filename or sanitize inputs.
54+
let safe_filename = format!("{}-{}", uuid::Uuid::new_v4(), file_name);
55+
let path = Path::new("./uploads").join(&safe_filename);
3356

34-
// Security: Create a safe random filename to prevent overwrites or path traversal
35-
let new_filename = format!("{}-{}", uuid::Uuid::new_v4(), file_name);
36-
let path = std::path::Path::new("./uploads").join(new_filename);
57+
println!("Streaming file: {} -> {:?}", file_name, path);
3758

3859
// Open destination file
39-
let mut file = File::create(&path).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?;
60+
let mut file = File::create(&path).await.map_err(|e| ApiError::internal(e.to_string()))?;
4061

41-
// Write stream to file chunk by chunk
42-
// In RustAPI/Axum multipart, `field.bytes()` loads the whole field into memory.
43-
// To stream efficiently, we use `field.chunk()`:
44-
45-
while let Some(chunk) = field.chunk().await.map_err(|_| ApiError::BadRequest)? {
46-
file.write_all(&chunk).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?;
62+
// Stream the field content chunk-by-chunk
63+
// This is memory efficient even for large files.
64+
while let Some(chunk) = field.chunk().await.map_err(|_| ApiError::bad_request("Stream error"))? {
65+
file.write_all(&chunk).await.map_err(|e| ApiError::internal(e.to_string()))?;
4766
}
67+
68+
uploaded_files.push(FileResult {
69+
original_name: file_name,
70+
stored_name: safe_filename,
71+
content_type,
72+
});
4873
}
4974

50-
Ok(StatusCode::CREATED)
75+
Ok(Json(UploadResponse {
76+
message: "Upload successful".into(),
77+
files: uploaded_files,
78+
}))
79+
}
80+
81+
#[derive(Serialize, Schema)]
82+
struct UploadResponse {
83+
message: String,
84+
files: Vec<FileResult>,
85+
}
86+
87+
#[derive(Serialize, Schema)]
88+
struct FileResult {
89+
original_name: String,
90+
stored_name: String,
91+
content_type: String,
5192
}
5293
```
5394

54-
## Handling Constraints
95+
## Key Concepts
5596

56-
You should always set limits to prevent DoS attacks.
97+
### 1. Streaming vs Buffering
98+
By default, some frameworks load the entire file into RAM. RustAPI's `Multipart` allows you to process the stream incrementally using `field.chunk()`.
99+
- **Buffering**: `field.bytes().await` (Load all into RAM - simple but dangerous for large files)
100+
- **Streaming**: `field.chunk().await` (Load small chunks - scalable)
57101

58-
```rust
59-
use rustapi_rs::extract::DefaultBodyLimit;
102+
### 2. Body Limits
103+
The default request body limit is often small (e.g., 2MB) to prevent DoS attacks. You must explicitly increase this limit for file upload routes using `DefaultBodyLimit::max(size)`.
60104

61-
let app = RustApi::new()
62-
.route("/upload", post(upload_file))
63-
// Limit request body to 10MB
64-
.layer(DefaultBodyLimit::max(10 * 1024 * 1024));
65-
```
105+
### 3. Security
106+
- **Path Traversal**: Malicious users can send filenames like `../../system32/cmd.exe`. Always rename files or sanitize filenames strictly.
107+
- **Content Type Validation**: The `Content-Type` header is client-controlled and can be spoofed. Do not rely on it for security execution checks (e.g., preventing `.php` execution).
108+
- **Executable Permissions**: Store uploads in a directory where script execution is disabled.
66109

67-
## Validating Content Type
110+
## Testing with cURL
68111

69-
Never trust the `Content-Type` header sent by the client implicitly for security (e.g., executing a PHP script uploaded as an image).
112+
You can test this endpoint using `curl`:
70113

71-
Verify the "magic bytes" of the file content itself if strictly needed, or ensure uploaded files are stored in a non-executable directory (or S3 bucket).
114+
```bash
115+
curl -X POST http://localhost:8080/upload \
116+
-F "file1=@./image.png" \
117+
-F "file2=@./document.pdf"
118+
```
72119

73-
```rust
74-
// Simple check on the header (not fully secure but good UX)
75-
if let Some(ct) = field.content_type() {
76-
if !ct.starts_with("image/") {
77-
return Err(ApiError::BadRequest("Only images are allowed".into()));
78-
}
120+
Response:
121+
```json
122+
{
123+
"message": "Upload successful",
124+
"files": [
125+
{
126+
"original_name": "image.png",
127+
"stored_name": "550e8400-e29b-41d4-a716-446655440000-image.png",
128+
"content_type": "image/png"
129+
},
130+
...
131+
]
79132
}
80133
```

0 commit comments

Comments
 (0)