diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index afd74ff51..1eaf3645d 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -7,6 +7,7 @@ The changelog for SRS. ## SRS 7.0 Changelog +* 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/app/srs_app_rtc_api.cpp b/trunk/src/app/srs_app_rtc_api.cpp index 4b5f3d2e5..23aa75c69 100644 --- a/trunk/src/app/srs_app_rtc_api.cpp +++ b/trunk/src/app/srs_app_rtc_api.cpp @@ -554,12 +554,12 @@ srs_error_t SrsGoApiRtcPublish::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMe } if ((err = security_->check(SrsRtcConnPublish, ruc->req_->ip_, ruc->req_)) != srs_success) { - return srs_error_wrap(err, "RTC: security check"); + return srs_error_transform(ERROR_SYSTEM_AUTH, err, "RTC: security check"); } // We must do hook after stat, because depends on it. if ((err = http_hooks_on_publish(ruc->req_)) != srs_success) { - return srs_error_wrap(err, "RTC: http_hooks_on_publish"); + return srs_error_transform(ERROR_SYSTEM_AUTH, err, "RTC: http_hooks_on_publish"); } ostringstream os; @@ -661,6 +661,44 @@ SrsGoApiRtcWhip::~SrsGoApiRtcWhip() } srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r) +{ + int code = 0; + string code_str; + if (true) { + srs_error_t err = srs_success; + + err = serve_http_with(w, r); + if (err == srs_success) { + return err; + } + + code = srs_error_code(err); + code_str = srs_error_code_str(err); + srs_warn("WHIP: serve http for %s with err %d:%s, %s", + r->url().c_str(), code, code_str.c_str(), srs_error_desc(err).c_str()); + srs_freep(err); + } + + if (code == ERROR_RTC_INVALID_SDP || code == ERROR_RTC_SDP_DECODE || code == ERROR_RTC_SDP_EXCHANGE) { + string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str()); + return srs_go_http_error(w, SRS_CONSTS_HTTP_BadRequest, msg); + } + + if (code == ERROR_SYSTEM_STREAM_BUSY) { + string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str()); + return srs_go_http_error(w, SRS_CONSTS_HTTP_Conflict, msg); + } + + if (code == ERROR_SYSTEM_AUTH) { + string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str()); + return srs_go_http_error(w, SRS_CONSTS_HTTP_Unauthorized, msg); + } + + string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str()); + return srs_go_http_error(w, SRS_CONSTS_HTTP_InternalServerError, msg); +} + +srs_error_t SrsGoApiRtcWhip::serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r) { srs_error_t err = srs_success; @@ -691,14 +729,14 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa } SrsRtcUserConfig ruc; - if ((err = do_serve_http(w, r, &ruc)) != srs_success) { + if ((err = do_serve_http_with(w, r, &ruc)) != srs_success) { return srs_error_wrap(err, "serve"); } - if (ruc.local_sdp_str_.empty()) { - return srs_go_http_error(w, SRS_CONSTS_HTTP_InternalServerError); - } // The SDP to response. + if (ruc.local_sdp_str_.empty()) { + return srs_error_new(ERROR_RTC_INVALID_SDP, "empty local sdp"); + } string sdp = ruc.local_sdp_str_; // Setup the content type to SDP. @@ -714,7 +752,7 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa return w->write((char *)sdp.data(), (int)sdp.length()); } -srs_error_t SrsGoApiRtcWhip::do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc) +srs_error_t SrsGoApiRtcWhip::do_serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc) { srs_error_t err = srs_success; @@ -796,6 +834,9 @@ srs_error_t SrsGoApiRtcWhip::do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMe // TODO: FIXME: It seems remote_sdp doesn't represents the full SDP information. ruc->remote_sdp_str_ = remote_sdp_str; + if (ruc->remote_sdp_str_.empty()) { + return srs_error_new(ERROR_RTC_INVALID_SDP, "empty remote sdp"); + } if ((err = ruc->remote_sdp_.parse(remote_sdp_str)) != srs_success) { return srs_error_wrap(err, "parse sdp failed: %s", remote_sdp_str.c_str()); } diff --git a/trunk/src/app/srs_app_rtc_api.hpp b/trunk/src/app/srs_app_rtc_api.hpp index d402eb43e..18ac4390d 100644 --- a/trunk/src/app/srs_app_rtc_api.hpp +++ b/trunk/src/app/srs_app_rtc_api.hpp @@ -119,7 +119,8 @@ public: // clang-format off SRS_DECLARE_PRIVATE: // clang-format on - virtual srs_error_t do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc); + virtual srs_error_t serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r); + virtual srs_error_t do_serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc); }; class SrsGoApiRtcNACK : public ISrsHttpHandler diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 9ccc2ecd5..c9a8684cc 100644 --- a/trunk/src/core/srs_core_version7.hpp +++ b/trunk/src/core/srs_core_version7.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 7 #define VERSION_MINOR 0 -#define VERSION_REVISION 120 +#define VERSION_REVISION 121 #endif \ No newline at end of file diff --git a/trunk/src/kernel/srs_kernel_error.cpp b/trunk/src/kernel/srs_kernel_error.cpp index c6bfd620d..00c31c6fe 100644 --- a/trunk/src/kernel/srs_kernel_error.cpp +++ b/trunk/src/kernel/srs_kernel_error.cpp @@ -327,18 +327,21 @@ SrsCplxError *SrsCplxError::wrap(const char *func, const char *file, int line, S int r0 = vsnprintf(buffer, maxLogBuf, fmt, ap); va_end(ap); + // Ensure buffer is null-terminated even if vsnprintf failed or truncated. + if (r0 < 0) { + buffer[0] = '\0'; + } else if (r0 >= maxLogBuf) { + buffer[maxLogBuf - 1] = '\0'; + } + SrsCplxError *err = new SrsCplxError(); err->func_ = func; err->file_ = file; err->line_ = line; - if (v) { - err->code_ = v->code_; - } + err->code_ = srs_error_code(v); err->rerrno_ = rerrno; - if (r0 > 0 && r0 < maxLogBuf) { - err->msg_ = string(buffer, r0); - } + err->msg_ = string(buffer); err->wrapped_ = v; if (_srs_context) { err->cid_ = _srs_context->get_id(); @@ -347,6 +350,29 @@ SrsCplxError *SrsCplxError::wrap(const char *func, const char *file, int line, S return err; } +SrsCplxError *SrsCplxError::transform(const char *func, const char *file, int line, int code, SrsCplxError *v, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + static char *buffer = new char[maxLogBuf]; + int r0 = vsnprintf(buffer, maxLogBuf, fmt, ap); + va_end(ap); + + // Ensure buffer is null-terminated even if vsnprintf failed or truncated. + if (r0 < 0) { + buffer[0] = '\0'; + } else if (r0 >= maxLogBuf) { + buffer[maxLogBuf - 1] = '\0'; + } + + // Wrap the error with additional context showing the code transformation. + SrsCplxError *err = NULL; + err = wrap(func, file, line, v, "code transformed (%d => %d): %s", srs_error_code(v), code, buffer); + err->code_ = code; + + return err; +} + SrsCplxError *SrsCplxError::success() { return NULL; diff --git a/trunk/src/kernel/srs_kernel_error.hpp b/trunk/src/kernel/srs_kernel_error.hpp index 67b79846c..0fe936f32 100644 --- a/trunk/src/kernel/srs_kernel_error.hpp +++ b/trunk/src/kernel/srs_kernel_error.hpp @@ -111,7 +111,9 @@ XX(ERROR_STREAM_DISPOSING, 1098, "StreamDisposing", "Stream is disposing") \ XX(ERROR_NOT_IMPLEMENTED, 1099, "NotImplemented", "Feature is not implemented") \ XX(ERROR_NOT_SUPPORTED, 1100, "NotSupported", "Feature is not supported") \ - XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file") + XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file") \ + XX(ERROR_SYSTEM_AUTH, 1102, "SystemAuth", "Failed to authenticate stream") + /**************************************************/ /* RTMP protocol error. */ @@ -380,7 +382,8 @@ XX(ERROR_RTSP_NO_TRACK, 5039, "RtspNoTrack", "Drop RTSP packet for track not found") \ XX(ERROR_RTSP_TOKEN_NOT_NORMAL, 5040, "RtspToken", "Invalid RTSP token state not normal") \ XX(ERROR_RTSP_REQUEST_HEADER_EOF, 5041, "RtspHeaderEof", "Invalid RTSP request for header EOF") \ - XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") + XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") \ + XX(ERROR_RTC_INVALID_SDP, 5043, "RtcInvalidSdp", "Invalid SDP for RTC") /**************************************************/ /* SRT protocol error. */ @@ -465,6 +468,7 @@ SRS_DECLARE_PRIVATE: // clang-format on public: static SrsCplxError *create(const char *func, const char *file, int line, int code, const char *fmt, ...); static SrsCplxError *wrap(const char *func, const char *file, int line, SrsCplxError *err, const char *fmt, ...); + static SrsCplxError *transform(const char *func, const char *file, int line, int code, SrsCplxError *err, const char *fmt, ...); static SrsCplxError *success(); static SrsCplxError *copy(SrsCplxError *from); static std::string description(SrsCplxError *err); @@ -479,13 +483,73 @@ public: // Error helpers, should use these functions to new or wrap an error. #define srs_success NULL // SrsCplxError::success() -#define srs_error_new(ret, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, ret, fmt, ##__VA_ARGS__) + +// Create a new error with the specified error code and message. +// +// Example: +// if (fd < 0) { +// return srs_error_new(ERROR_SOCKET_CREATE, "create socket fd=%d", fd); +// } +#define srs_error_new(code, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__) + +// Wrap an existing error with additional context. The error code is +// preserved from the wrapped error. +// +// Example: +// if ((err = do_connect(host, port)) != srs_success) { +// return srs_error_wrap(err, "connect to %s:%d", host.c_str(), port); +// } #define srs_error_wrap(err, fmt, ...) SrsCplxError::wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__) + +// Transform an error by wrapping it and changing its error code. Useful +// for converting internal errors to protocol-specific error codes (e.g., +// HTTP, RTMP). The wrapped error chain is preserved. +// +// Example: +// if ((err = http_hooks_on_publish(ruc->req_)) != srs_success) { +// return srs_error_transform(ERROR_SYSTEM_AUTH, err, "RTC: http_hooks_on_publish"); +// } +#define srs_error_transform(code, err, fmt, ...) SrsCplxError::transform(__FUNCTION__, __FILE__, __LINE__, code, err, fmt, ##__VA_ARGS__) + +// Copy an error object. Returns a new error object with the same content. +// +// Example: +// srs_error_t err_copy = srs_error_copy(err); #define srs_error_copy(err) SrsCplxError::copy(err) + +// Get the full description of an error including the entire error chain. +// +// Example: +// srs_error_t err = do_something(); +// srs_warn("error: %s", srs_error_desc(err).c_str()); #define srs_error_desc(err) SrsCplxError::description(err) + +// Get a brief summary of an error (only the top-level message). +// +// Example: +// srs_error_t err = do_something(); +// srs_trace("error summary: %s", srs_error_summary(err).c_str()); #define srs_error_summary(err) SrsCplxError::summary(err) + +// Get the error code as an integer. +// +// Example: +// int code = srs_error_code(err); +// if (code == ERROR_SOCKET_TIMEOUT) { ... } #define srs_error_code(err) SrsCplxError::error_code(err) + +// Get the error code as a short string (e.g., "SocketTimeout"). +// +// Example: +// string code_str = srs_error_code_str(err); +// srs_trace("error code: %s", code_str.c_str()); #define srs_error_code_str(err) SrsCplxError::error_code_str(err) + +// Get the error code description (e.g., "Socket io timeout"). +// +// Example: +// string desc = srs_error_code_strlong(err); +// srs_warn("error description: %s", desc.c_str()); #define srs_error_code_strlong(err) SrsCplxError::error_code_strlong(err) #ifndef srs_assert 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 0e8385e53..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) @@ -2437,7 +2437,7 @@ VOID TEST(SrsGoApiRtcWhipTest, DoServeHttpPublishSuccess) SrsUniquePtr ruc(new SrsRtcUserConfig()); // Call do_serve_http - major use scenario - HELPER_EXPECT_SUCCESS(whip->do_serve_http(mock_writer.get(), mock_request.get(), ruc.get())); + HELPER_EXPECT_SUCCESS(whip->do_serve_http_with(mock_writer.get(), mock_request.get(), ruc.get())); // Verify request fields were populated correctly EXPECT_STREQ("192.168.1.100", ruc->req_->ip_.c_str()); @@ -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():