Skip to content

Commit 0a616c7

Browse files
committed
feat: streaming response support + CI tests
Signed-off-by: Xin Liu <[email protected]>
1 parent 23be412 commit 0a616c7

File tree

8 files changed

+601
-12
lines changed

8 files changed

+601
-12
lines changed

.github/workflows/test.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ on:
1717
- '**/*.rs'
1818
- '**/*.hurl'
1919
- 'run_tests.sh'
20+
- 'test_streaming.sh'
2021
- 'tests/**'
2122
pull_request:
2223
branches: [ main ]
@@ -252,3 +253,79 @@ jobs:
252253
docker rm json-api || true
253254
docker stop ws-echo || true
254255
docker rm ws-echo || true
256+
257+
streaming-test:
258+
name: Streaming Response Test
259+
runs-on: ubuntu-latest
260+
strategy:
261+
matrix:
262+
rust: [1.90.0]
263+
264+
steps:
265+
- name: Checkout code
266+
uses: actions/checkout@v5
267+
268+
- name: Install Rust-stable
269+
uses: actions-rust-lang/setup-rust-toolchain@v1
270+
with:
271+
toolchain: ${{ matrix.rust }}
272+
273+
- name: Cache Rust dependencies
274+
uses: Swatinem/rust-cache@v2
275+
with:
276+
cache-on-failure: true
277+
278+
- name: Install Python dependencies
279+
run: |
280+
python3 --version
281+
pip3 --version
282+
283+
- name: Install SQLite
284+
run: sudo apt-get update && sudo apt-get install -y sqlite3
285+
286+
- name: Install jq (for JSON parsing in tests)
287+
run: sudo apt-get install -y jq
288+
289+
- name: Verify tools
290+
run: |
291+
echo "Rust version:"
292+
cargo --version
293+
rustc --version
294+
echo ""
295+
echo "Python version:"
296+
python3 --version
297+
echo ""
298+
echo "SQLite version:"
299+
sqlite3 --version
300+
echo ""
301+
echo "jq version:"
302+
jq --version
303+
304+
- name: Run streaming tests
305+
env:
306+
CI: true
307+
TEST_PROXY_PORT: 8080
308+
TEST_MOCK_PORT: 10086
309+
run: |
310+
chmod +x test_streaming.sh
311+
./test_streaming.sh
312+
313+
- name: Upload test logs on failure
314+
if: failure()
315+
uses: actions/upload-artifact@v4
316+
with:
317+
name: streaming-test-logs
318+
path: |
319+
/tmp/proxy.log
320+
/tmp/mock-server.log
321+
test_streaming.db
322+
retention-days: 7
323+
324+
- name: Display logs on failure
325+
if: failure()
326+
run: |
327+
echo "=== Proxy Logs ==="
328+
cat /tmp/proxy.log || echo "No proxy log found"
329+
echo ""
330+
echo "=== Mock Server Logs ==="
331+
cat /tmp/mock-server.log || echo "No mock server log found"

Cargo.lock

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ clap = { version = "4.5", features = ["derive", "env"] }
1515
futures-util = "0.3"
1616

1717
# HTTP client - Using rustls instead of native-tls/openssl
18-
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
18+
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"], default-features = false }
1919

2020
# Serialization
2121
serde = { version = "1.0", features = ["derive"] }

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ A high-performance proxy server built with Rust, supporting HTTP/HTTPS and WebSo
3636
- [Option 1: With Docker Services (Recommended)](#option-1-with-docker-services-recommended)
3737
- [Option 2: Manual Service Management](#option-2-manual-service-management)
3838
- [Option 3: Individual Test Suites](#option-3-individual-test-suites)
39+
- [Streaming Tests](#streaming-tests)
3940
- [Test Services](#test-services)
4041
- [Benefits of Docker-Based Testing](#benefits-of-docker-based-testing)
4142
- [CI/CD](#cicd)
@@ -56,6 +57,7 @@ A high-performance proxy server built with Rust, supporting HTTP/HTTPS and WebSo
5657

5758
- 🚀 **High-Performance Async Proxy**: Built on Tokio and Axum
5859
- 🔄 **Protocol Support**: Supports HTTP/HTTPS and WebSocket proxying
60+
- 🌊 **Streaming Support**: Native support for streaming responses (SSE, LLM APIs, chunked encoding)
5961
- 💾 **Session Management**: Uses SQLite database to store session information
6062
- 🎯 **Dynamic Routing**: Dynamically forwards to different downstream servers based on session_id
6163
-**Connection Pooling**: Built-in database connection pool and HTTP client connection pool
@@ -401,10 +403,33 @@ cargo test --test integration
401403

402404
# Hurl HTTP API tests only (requires services running)
403405
hurl --test --variable port=8080 tests/http.hurl
406+
407+
# Streaming response tests (独立测试)
408+
./test_streaming.sh
404409
```
405410

406411
**Note:** WebSocket tests are only available in Rust integration tests (`tests/integration.rs`), as Hurl doesn't support WebSocket message protocol.
407412

413+
#### Streaming Tests
414+
415+
流式传输测试验证 ss-proxy 对流式响应的支持,包括 LLM API(如 OpenAI)的流式输出:
416+
417+
```bash
418+
# 运行完整的流式传输测试套件
419+
./test_streaming.sh
420+
421+
# 使用自定义端口
422+
TEST_PROXY_PORT=9090 TEST_MOCK_PORT=10087 ./test_streaming.sh
423+
```
424+
425+
测试覆盖:
426+
- ✅ 非流式请求转发 (`stream=false`)
427+
- ✅ 流式请求转发 (`stream=true`)
428+
- ✅ SSE (Server-Sent Events) 格式验证
429+
- ✅ 首字节延迟 (TTFB) 性能测试
430+
431+
详见:[流式传输测试文档](tests/STREAMING_TEST_README.md)
432+
408433
### Test Services
409434

410435
The test suite uses the following Docker services (all run locally):
@@ -427,10 +452,17 @@ All services are automatically managed by `run_tests.sh` when `USE_DOCKER_SERVIC
427452

428453
### CI/CD
429454

430-
The project includes a GitHub Actions workflow (`.github/workflows/test.yml`) that automatically:
455+
The project includes GitHub Actions workflows that automatically:
456+
457+
**Test Workflow** (`.github/workflows/test.yml`):
431458
- Runs all tests on push/PR
459+
- Two independent test jobs:
460+
- `test`: HTTP/HTTPS and WebSocket proxy tests
461+
- `streaming-test`: Streaming response tests (新增)
432462
- Uses service containers for test dependencies
433463
- Caches Rust dependencies for faster builds
464+
465+
**Build Workflow** (`.github/workflows/build.yml`):
434466
- Runs linting and formatting checks
435467
- Builds binaries for multiple platforms
436468

src/proxy/http_proxy.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use futures_util::StreamExt;
12
use reqwest::{Client, Method, Request, Url};
23
use std::time::Duration;
34
use tracing::{error, info};
@@ -78,15 +79,18 @@ impl HttpProxy {
7879
builder = builder.header(key, value);
7980
}
8081

81-
// Get response body
82-
let body_bytes = response.bytes().await.map_err(|e| {
83-
error!("Failed to read response body: {}", e);
84-
ProxyError::ResponseReadFailed(e.to_string())
85-
})?;
82+
// Convert reqwest response stream to axum body for streaming support
83+
let stream = response.bytes_stream();
84+
let body_stream = stream.map(|result| {
85+
result.map_err(|e| {
86+
error!("Error reading response stream: {}", e);
87+
std::io::Error::new(std::io::ErrorKind::Other, e)
88+
})
89+
});
8690

87-
// Build final response
91+
// Build final response with streaming body
8892
let final_response = builder
89-
.body(axum::body::Body::from(body_bytes))
93+
.body(axum::body::Body::from_stream(body_stream))
9094
.map_err(|e| {
9195
error!("Failed to build response: {}", e);
9296
ProxyError::ResponseBuildFailed(e.to_string())
@@ -105,9 +109,6 @@ pub enum ProxyError {
105109
#[error("Request failed: {0}")]
106110
RequestFailed(String),
107111

108-
#[error("Failed to read response: {0}")]
109-
ResponseReadFailed(String),
110-
111112
#[error("Failed to build response: {0}")]
112113
ResponseBuildFailed(String),
113114
}

0 commit comments

Comments
 (0)