diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md
index 748bdc545..1eaf3645d 100644
--- a/trunk/doc/CHANGELOG.md
+++ b/trunk/doc/CHANGELOG.md
@@ -7,7 +7,7 @@ The changelog for SRS.
## SRS 7.0 Changelog
-* v7.0, 2025-11-07, AI: Kernel: Add srs_error_transform to wrap errors with new error codes. v7.0.121 (#4502)
+* v7.0, 2025-11-07, AI: WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502)
* v7.0, 2025-11-07, AI: HLS: Support query string in hls_key_url for JWT tokens. v7.0.120 (#4426)
* v7.0, 2025-11-07, AI: RTC: Support keep_original_ssrc to preserve SSRC and timestamps. v7.0.119 (#3850)
* v7.0, 2025-11-05, AI: WebRTC: Report video/audio codec info and frame stats in HTTP API. v7.0.118 (#4554)
diff --git a/trunk/src/utest/srs_utest_ai05.cpp b/trunk/src/utest/srs_utest_ai05.cpp
index 7d838d617..f23d9a90d 100644
--- a/trunk/src/utest/srs_utest_ai05.cpp
+++ b/trunk/src/utest/srs_utest_ai05.cpp
@@ -1142,6 +1142,74 @@ VOID TEST(KernelErrorTest, ErrorChaining)
srs_freep(level3);
}
+VOID TEST(KernelErrorTest, SrsCplxErrorTransform)
+{
+ srs_error_t err;
+
+ // Test transform with real error - changes error code
+ srs_error_t original = srs_error_new(ERROR_SYSTEM_STREAM_BUSY, "stream busy");
+ err = srs_error_transform(ERROR_SYSTEM_AUTH, original, "authentication failed");
+
+ EXPECT_TRUE(err != srs_success);
+ EXPECT_EQ(ERROR_SYSTEM_AUTH, srs_error_code(err)); // Should have new code
+
+ // Check description contains both messages and code transformation info
+ std::string desc = srs_error_desc(err);
+ EXPECT_TRUE(desc.find("authentication failed") != std::string::npos);
+ EXPECT_TRUE(desc.find("stream busy") != std::string::npos);
+ EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
+ EXPECT_TRUE(desc.find("1028") != std::string::npos); // Original code ERROR_SYSTEM_STREAM_BUSY
+ EXPECT_TRUE(desc.find("1102") != std::string::npos); // New code ERROR_SYSTEM_AUTH
+
+ srs_freep(err);
+
+ // Test transform with NULL error
+ err = srs_error_transform(ERROR_HTTP_STATUS_INVALID, srs_success, "transform null error");
+ EXPECT_TRUE(err != srs_success);
+ EXPECT_EQ(ERROR_HTTP_STATUS_INVALID, srs_error_code(err)); // Should have specified code
+
+ desc = srs_error_desc(err);
+ EXPECT_TRUE(desc.find("transform null error") != std::string::npos);
+ EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
+
+ srs_freep(err);
+
+ // Test transform with formatted message
+ srs_error_t inner = srs_error_new(ERROR_RTC_SDP_DECODE, "invalid sdp format");
+ err = srs_error_transform(ERROR_HTTP_STATUS_INVALID, inner, "http error: %s", "bad request");
+
+ EXPECT_TRUE(err != srs_success);
+ EXPECT_EQ(ERROR_HTTP_STATUS_INVALID, srs_error_code(err));
+
+ desc = srs_error_desc(err);
+ EXPECT_TRUE(desc.find("http error: bad request") != std::string::npos);
+ EXPECT_TRUE(desc.find("invalid sdp format") != std::string::npos);
+ EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
+ // Verify the code transformation shows both old and new codes
+ std::string code_transform_msg = srs_fmt_sprintf("(%d => %d)", ERROR_RTC_SDP_DECODE, ERROR_HTTP_STATUS_INVALID);
+ EXPECT_TRUE(desc.find(code_transform_msg) != std::string::npos);
+
+ srs_freep(err);
+
+ // Test chaining: wrap -> transform -> wrap
+ srs_error_t level1 = srs_error_new(ERROR_SOCKET_READ, "socket read failed");
+ srs_error_t level2 = srs_error_wrap(level1, "connection error");
+ srs_error_t level3 = srs_error_transform(ERROR_SYSTEM_AUTH, level2, "auth check failed");
+ srs_error_t level4 = srs_error_wrap(level3, "publish rejected");
+
+ EXPECT_TRUE(level4 != srs_success);
+ EXPECT_EQ(ERROR_SYSTEM_AUTH, srs_error_code(level4)); // Should have transformed code
+
+ desc = srs_error_desc(level4);
+ EXPECT_TRUE(desc.find("socket read failed") != std::string::npos);
+ EXPECT_TRUE(desc.find("connection error") != std::string::npos);
+ EXPECT_TRUE(desc.find("auth check failed") != std::string::npos);
+ EXPECT_TRUE(desc.find("publish rejected") != std::string::npos);
+ EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
+
+ srs_freep(level4);
+}
+
VOID TEST(KernelErrorTest, ErrorDescriptionFormatting)
{
srs_error_t err;
diff --git a/trunk/src/utest/srs_utest_ai17.cpp b/trunk/src/utest/srs_utest_ai17.cpp
index cb36a7e7a..5937bf5bd 100644
--- a/trunk/src/utest/srs_utest_ai17.cpp
+++ b/trunk/src/utest/srs_utest_ai17.cpp
@@ -2254,7 +2254,7 @@ VOID TEST(SrsGoApiRtcWhipTest, ServeHttpDeleteSuccess)
// Test SrsGoApiRtcWhip::serve_http() to verify the major use scenario for WHIP POST request.
// This test covers the WHIP session creation flow (non-DELETE path):
// 1. Client sends POST request with SDP offer in body
-// 2. Server processes the request via do_serve_http() which populates ruc.local_sdp_str_
+// 2. Server processes the request via do_serve_http_with() which populates ruc.local_sdp_str_
// 3. Server returns 201 Created with SDP answer in body
// 4. Server includes Location header for subsequent DELETE request
// 5. Server sets Content-Type to application/sdp
@@ -2265,14 +2265,14 @@ VOID TEST(SrsGoApiRtcWhipTest, ServeHttpPostSuccess)
// Create mock RTC API server
SrsUniquePtr mock_server(new MockRtcApiServer());
- // Create testable WHIP handler that overrides do_serve_http
+ // Create testable WHIP handler that overrides do_serve_http_with
class TestableWhip : public SrsGoApiRtcWhip
{
public:
TestableWhip(ISrsRtcApiServer *server) : SrsGoApiRtcWhip(server) {}
- virtual srs_error_t do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc)
+ virtual srs_error_t do_serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc)
{
- // Mock the do_serve_http behavior by populating the required fields
+ // Mock the do_serve_http_with behavior by populating the required fields
ruc->local_sdp_str_ = "v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=SRS\r\nt=0 0\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\na=rtpmap:96 H264/90000\r\n";
ruc->session_id_ = "test-session-12345";
ruc->token_ = "test-token-67890";
@@ -2304,7 +2304,7 @@ VOID TEST(SrsGoApiRtcWhipTest, ServeHttpPostSuccess)
// Call serve_http for POST request
// Expected behavior:
// 1. Check if method is DELETE (no, it's POST)
- // 2. Call do_serve_http() which populates ruc.local_sdp_str_
+ // 2. Call do_serve_http_with() which populates ruc.local_sdp_str_
// 3. Set Content-Type to application/sdp
// 4. Set Location header with session and token
// 5. Return 201 Created with SDP answer in body
@@ -2330,8 +2330,8 @@ VOID TEST(SrsGoApiRtcWhipTest, ServeHttpPostSuccess)
EXPECT_TRUE(response.find("m=video 9 UDP/TLS/RTP/SAVPF 96") != std::string::npos);
}
-// Test SrsGoApiRtcWhip::do_serve_http() - major use scenario for WHIP request parsing
-// This test covers the core parsing and validation logic of do_serve_http method:
+// Test SrsGoApiRtcWhip::do_serve_http_with() - major use scenario for WHIP request parsing
+// This test covers the core parsing and validation logic of do_serve_http_with method:
// 1. Read SDP offer from request body
// 2. Extract client IP from connection (with proxy IP override support)
// 3. Parse query parameters (eip, codec, app, stream, action, ice-ufrag, ice-pwd, encrypt, dtls)
@@ -2466,6 +2466,186 @@ VOID TEST(SrsGoApiRtcWhipTest, DoServeHttpPublishSuccess)
whip->config_ = NULL;
}
+// Test SrsGoApiRtcWhip::serve_http() error handling for invalid SDP.
+// This test verifies that WHIP returns HTTP 400 Bad Request when SDP parsing fails.
+VOID TEST(SrsGoApiRtcWhipTest, ServeHttpErrorInvalidSdp)
+{
+ srs_error_t err = srs_success;
+
+ // Create mock RTC API server
+ SrsUniquePtr mock_server(new MockRtcApiServer());
+
+ // Create testable WHIP handler that simulates SDP parsing error
+ class TestableWhip : public SrsGoApiRtcWhip
+ {
+ public:
+ TestableWhip(ISrsRtcApiServer *server) : SrsGoApiRtcWhip(server) {}
+ virtual srs_error_t serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r)
+ {
+ return srs_error_new(ERROR_RTC_SDP_DECODE, "invalid sdp format");
+ }
+ };
+
+ // Create testable WHIP instance
+ SrsUniquePtr whip(new TestableWhip(mock_server.get()));
+
+ // Create mock response writer
+ SrsUniquePtr mock_writer(new MockResponseWriter());
+
+ // Create mock HTTP message for WHIP POST request
+ SrsUniquePtr mock_request(new MockHttpMessageForRtcApi());
+ mock_request->set_method(SRS_CONSTS_HTTP_POST);
+
+ // Call serve_http - should return HTTP 400 Bad Request
+ HELPER_EXPECT_SUCCESS(whip->serve_http(mock_writer.get(), mock_request.get()));
+
+ // Verify response status is 400 Bad Request
+ EXPECT_EQ(SRS_CONSTS_HTTP_BadRequest, mock_writer->w->status_);
+
+ // Get the HTTP response
+ string response = string(mock_writer->io.out_buffer.bytes(), mock_writer->io.out_buffer.length());
+ EXPECT_FALSE(response.empty());
+
+ // Verify the response contains error code and description
+ EXPECT_TRUE(response.find("5012") != std::string::npos); // ERROR_RTC_SDP_DECODE
+ EXPECT_TRUE(response.find("RtcSdpDecode") != std::string::npos);
+}
+
+// Test SrsGoApiRtcWhip::serve_http() error handling for stream busy.
+// This test verifies that WHIP returns HTTP 409 Conflict when stream is already publishing.
+VOID TEST(SrsGoApiRtcWhipTest, ServeHttpErrorStreamBusy)
+{
+ srs_error_t err = srs_success;
+
+ // Create mock RTC API server
+ SrsUniquePtr mock_server(new MockRtcApiServer());
+
+ // Create testable WHIP handler that simulates stream busy error
+ class TestableWhip : public SrsGoApiRtcWhip
+ {
+ public:
+ TestableWhip(ISrsRtcApiServer *server) : SrsGoApiRtcWhip(server) {}
+ virtual srs_error_t serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r)
+ {
+ return srs_error_new(ERROR_SYSTEM_STREAM_BUSY, "stream already publishing");
+ }
+ };
+
+ // Create testable WHIP instance
+ SrsUniquePtr whip(new TestableWhip(mock_server.get()));
+
+ // Create mock response writer
+ SrsUniquePtr mock_writer(new MockResponseWriter());
+
+ // Create mock HTTP message for WHIP POST request
+ SrsUniquePtr mock_request(new MockHttpMessageForRtcApi());
+ mock_request->set_method(SRS_CONSTS_HTTP_POST);
+
+ // Call serve_http - should return HTTP 409 Conflict
+ HELPER_EXPECT_SUCCESS(whip->serve_http(mock_writer.get(), mock_request.get()));
+
+ // Verify response status is 409 Conflict
+ EXPECT_EQ(SRS_CONSTS_HTTP_Conflict, mock_writer->w->status_);
+
+ // Get the HTTP response
+ string response = string(mock_writer->io.out_buffer.bytes(), mock_writer->io.out_buffer.length());
+ EXPECT_FALSE(response.empty());
+
+ // Verify the response contains error code and description
+ EXPECT_TRUE(response.find("1028") != std::string::npos); // ERROR_SYSTEM_STREAM_BUSY
+ EXPECT_TRUE(response.find("StreamBusy") != std::string::npos);
+}
+
+// Test SrsGoApiRtcWhip::serve_http() error handling for authentication failure.
+// This test verifies that WHIP returns HTTP 401 Unauthorized when auth check fails.
+VOID TEST(SrsGoApiRtcWhipTest, ServeHttpErrorAuth)
+{
+ srs_error_t err = srs_success;
+
+ // Create mock RTC API server
+ SrsUniquePtr mock_server(new MockRtcApiServer());
+
+ // Create testable WHIP handler that simulates auth error
+ class TestableWhip : public SrsGoApiRtcWhip
+ {
+ public:
+ TestableWhip(ISrsRtcApiServer *server) : SrsGoApiRtcWhip(server) {}
+ virtual srs_error_t serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r)
+ {
+ return srs_error_new(ERROR_SYSTEM_AUTH, "authentication failed");
+ }
+ };
+
+ // Create testable WHIP instance
+ SrsUniquePtr whip(new TestableWhip(mock_server.get()));
+
+ // Create mock response writer
+ SrsUniquePtr mock_writer(new MockResponseWriter());
+
+ // Create mock HTTP message for WHIP POST request
+ SrsUniquePtr mock_request(new MockHttpMessageForRtcApi());
+ mock_request->set_method(SRS_CONSTS_HTTP_POST);
+
+ // Call serve_http - should return HTTP 401 Unauthorized
+ HELPER_EXPECT_SUCCESS(whip->serve_http(mock_writer.get(), mock_request.get()));
+
+ // Verify response status is 401 Unauthorized
+ EXPECT_EQ(SRS_CONSTS_HTTP_Unauthorized, mock_writer->w->status_);
+
+ // Get the HTTP response
+ string response = string(mock_writer->io.out_buffer.bytes(), mock_writer->io.out_buffer.length());
+ EXPECT_FALSE(response.empty());
+
+ // Verify the response contains error code and description
+ EXPECT_TRUE(response.find("1102") != std::string::npos); // ERROR_SYSTEM_AUTH
+ EXPECT_TRUE(response.find("SystemAuth") != std::string::npos);
+}
+
+// Test SrsGoApiRtcWhip::serve_http() error handling for internal server error.
+// This test verifies that WHIP returns HTTP 500 for unexpected errors.
+VOID TEST(SrsGoApiRtcWhipTest, ServeHttpErrorInternal)
+{
+ srs_error_t err = srs_success;
+
+ // Create mock RTC API server
+ SrsUniquePtr mock_server(new MockRtcApiServer());
+
+ // Create testable WHIP handler that simulates internal error
+ class TestableWhip : public SrsGoApiRtcWhip
+ {
+ public:
+ TestableWhip(ISrsRtcApiServer *server) : SrsGoApiRtcWhip(server) {}
+ virtual srs_error_t serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r)
+ {
+ return srs_error_new(ERROR_SOCKET_TIMEOUT, "socket timeout");
+ }
+ };
+
+ // Create testable WHIP instance
+ SrsUniquePtr whip(new TestableWhip(mock_server.get()));
+
+ // Create mock response writer
+ SrsUniquePtr mock_writer(new MockResponseWriter());
+
+ // Create mock HTTP message for WHIP POST request
+ SrsUniquePtr mock_request(new MockHttpMessageForRtcApi());
+ mock_request->set_method(SRS_CONSTS_HTTP_POST);
+
+ // Call serve_http - should return HTTP 500 Internal Server Error
+ HELPER_EXPECT_SUCCESS(whip->serve_http(mock_writer.get(), mock_request.get()));
+
+ // Verify response status is 500 Internal Server Error
+ EXPECT_EQ(SRS_CONSTS_HTTP_InternalServerError, mock_writer->w->status_);
+
+ // Get the HTTP response
+ string response = string(mock_writer->io.out_buffer.bytes(), mock_writer->io.out_buffer.length());
+ EXPECT_FALSE(response.empty());
+
+ // Verify the response contains error code and description
+ EXPECT_TRUE(response.find("1011") != std::string::npos); // ERROR_SOCKET_TIMEOUT
+ EXPECT_TRUE(response.find("SocketTimeout") != std::string::npos);
+}
+
VOID TEST(RtcApiNackTest, ServeHttpSuccess)
{
// This test covers the major use scenario for SrsGoApiRtcNACK::serve_http():