Skip to content

Commit f392f9a

Browse files
winlinvipossrs-ai
andauthored
WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502) (#4562)
This commit addresses issue #4502 by implementing proper HTTP error handling for WHIP endpoints, allowing clients to receive detailed error information instead of empty responses. Before this change: - WHIP clients received "Empty reply from server" when publish failed - No way to distinguish between different failure reasons After this change: - WHIP clients receive proper HTTP status codes (400/401/409/500) - Error responses include error code and description - Clients can distinguish between SDP errors, stream busy, auth failures, etc. If success: ``` < HTTP/1.1 201 Created < Content-Type: application/sdp < Location: /rtc/v1/whip/?action=delete&token=77h5570j1&app=live&stream=livestream&session=x209e499:TKxW < Content-Length: 1376 < Server: SRS/7.0.120(Kai) < v=0 ...... ``` If request without SDP: ``` curl 'http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream' -v < HTTP/1.1 400 Bad Request < Content-Type: text/plain; charset=utf-8 < Content-Length: 13 < Server: SRS/7.0.120(Kai) < 5043: RtcInvalidSdp ``` If request with corrupt SDP: ``` curl 'http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream' --data-raw $'invalidsdp' -v < HTTP/1.1 400 Bad Request < Content-Type: text/plain; charset=utf-8 < Content-Length: 18 < Server: SRS/7.0.120(Kai) < 5012: RtcSdpDecode ``` If request with insufficient SDP: ``` curl 'http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream' --data-raw $'v=0' -v < HTTP/1.1 400 Bad Request < Content-Type: text/plain; charset=utf-8 < Content-Length: 21 < Server: SRS/7.0.120(Kai) < 5018: RtcSdpNegotiate ``` If publish to a exists stream: ``` < HTTP/1.1 409 Conflict < Content-Type: text/plain; charset=utf-8 < Content-Length: 16 < Server: SRS/7.0.120(Kai) < 1028: StreamBusy ``` If HTTP hooks or security verify failed: ``` < HTTP/1.1 401 Unauthorized < Content-Type: text/plain; charset=utf-8 < Content-Length: 16 < Server: SRS/7.0.120(Kai) < 1102: SystemAuth ``` Other errors, for exmaple, RTC disabled: ``` < HTTP/1.1 500 Internal Server Error < Content-Type: text/plain; charset=utf-8 < Content-Length: 17 < Server: SRS/7.0.120(Kai) < 5021: RtcDisabled ``` --------- Co-authored-by: OSSRS-AI <winlinam@gmail.com>
1 parent 99970d6 commit f392f9a

File tree

8 files changed

+407
-26
lines changed

8 files changed

+407
-26
lines changed

trunk/doc/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The changelog for SRS.
77
<a name="v7-changes"></a>
88

99
## SRS 7.0 Changelog
10+
* v7.0, 2025-11-07, AI: WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502)
1011
* v7.0, 2025-11-07, AI: HLS: Support query string in hls_key_url for JWT tokens. v7.0.120 (#4426)
1112
* v7.0, 2025-11-07, AI: RTC: Support keep_original_ssrc to preserve SSRC and timestamps. v7.0.119 (#3850)
1213
* v7.0, 2025-11-05, AI: WebRTC: Report video/audio codec info and frame stats in HTTP API. v7.0.118 (#4554)

trunk/src/app/srs_app_rtc_api.cpp

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -554,12 +554,12 @@ srs_error_t SrsGoApiRtcPublish::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMe
554554
}
555555

556556
if ((err = security_->check(SrsRtcConnPublish, ruc->req_->ip_, ruc->req_)) != srs_success) {
557-
return srs_error_wrap(err, "RTC: security check");
557+
return srs_error_transform(ERROR_SYSTEM_AUTH, err, "RTC: security check");
558558
}
559559

560560
// We must do hook after stat, because depends on it.
561561
if ((err = http_hooks_on_publish(ruc->req_)) != srs_success) {
562-
return srs_error_wrap(err, "RTC: http_hooks_on_publish");
562+
return srs_error_transform(ERROR_SYSTEM_AUTH, err, "RTC: http_hooks_on_publish");
563563
}
564564

565565
ostringstream os;
@@ -661,6 +661,44 @@ SrsGoApiRtcWhip::~SrsGoApiRtcWhip()
661661
}
662662

663663
srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r)
664+
{
665+
int code = 0;
666+
string code_str;
667+
if (true) {
668+
srs_error_t err = srs_success;
669+
670+
err = serve_http_with(w, r);
671+
if (err == srs_success) {
672+
return err;
673+
}
674+
675+
code = srs_error_code(err);
676+
code_str = srs_error_code_str(err);
677+
srs_warn("WHIP: serve http for %s with err %d:%s, %s",
678+
r->url().c_str(), code, code_str.c_str(), srs_error_desc(err).c_str());
679+
srs_freep(err);
680+
}
681+
682+
if (code == ERROR_RTC_INVALID_SDP || code == ERROR_RTC_SDP_DECODE || code == ERROR_RTC_SDP_EXCHANGE) {
683+
string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str());
684+
return srs_go_http_error(w, SRS_CONSTS_HTTP_BadRequest, msg);
685+
}
686+
687+
if (code == ERROR_SYSTEM_STREAM_BUSY) {
688+
string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str());
689+
return srs_go_http_error(w, SRS_CONSTS_HTTP_Conflict, msg);
690+
}
691+
692+
if (code == ERROR_SYSTEM_AUTH) {
693+
string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str());
694+
return srs_go_http_error(w, SRS_CONSTS_HTTP_Unauthorized, msg);
695+
}
696+
697+
string msg = srs_fmt_sprintf("%d: %s", code, code_str.c_str());
698+
return srs_go_http_error(w, SRS_CONSTS_HTTP_InternalServerError, msg);
699+
}
700+
701+
srs_error_t SrsGoApiRtcWhip::serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r)
664702
{
665703
srs_error_t err = srs_success;
666704

@@ -691,14 +729,14 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
691729
}
692730

693731
SrsRtcUserConfig ruc;
694-
if ((err = do_serve_http(w, r, &ruc)) != srs_success) {
732+
if ((err = do_serve_http_with(w, r, &ruc)) != srs_success) {
695733
return srs_error_wrap(err, "serve");
696734
}
697-
if (ruc.local_sdp_str_.empty()) {
698-
return srs_go_http_error(w, SRS_CONSTS_HTTP_InternalServerError);
699-
}
700735

701736
// The SDP to response.
737+
if (ruc.local_sdp_str_.empty()) {
738+
return srs_error_new(ERROR_RTC_INVALID_SDP, "empty local sdp");
739+
}
702740
string sdp = ruc.local_sdp_str_;
703741

704742
// Setup the content type to SDP.
@@ -714,7 +752,7 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa
714752
return w->write((char *)sdp.data(), (int)sdp.length());
715753
}
716754

717-
srs_error_t SrsGoApiRtcWhip::do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc)
755+
srs_error_t SrsGoApiRtcWhip::do_serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc)
718756
{
719757
srs_error_t err = srs_success;
720758

@@ -796,6 +834,9 @@ srs_error_t SrsGoApiRtcWhip::do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMe
796834

797835
// TODO: FIXME: It seems remote_sdp doesn't represents the full SDP information.
798836
ruc->remote_sdp_str_ = remote_sdp_str;
837+
if (ruc->remote_sdp_str_.empty()) {
838+
return srs_error_new(ERROR_RTC_INVALID_SDP, "empty remote sdp");
839+
}
799840
if ((err = ruc->remote_sdp_.parse(remote_sdp_str)) != srs_success) {
800841
return srs_error_wrap(err, "parse sdp failed: %s", remote_sdp_str.c_str());
801842
}

trunk/src/app/srs_app_rtc_api.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ class SrsGoApiRtcWhip : public ISrsHttpHandler
119119

120120
// clang-format off
121121
SRS_DECLARE_PRIVATE: // clang-format on
122-
virtual srs_error_t do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc);
122+
virtual srs_error_t serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r);
123+
virtual srs_error_t do_serve_http_with(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsRtcUserConfig *ruc);
123124
};
124125

125126
class SrsGoApiRtcNACK : public ISrsHttpHandler

trunk/src/core/srs_core_version7.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99

1010
#define VERSION_MAJOR 7
1111
#define VERSION_MINOR 0
12-
#define VERSION_REVISION 120
12+
#define VERSION_REVISION 121
1313

1414
#endif

trunk/src/kernel/srs_kernel_error.cpp

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,18 +327,21 @@ SrsCplxError *SrsCplxError::wrap(const char *func, const char *file, int line, S
327327
int r0 = vsnprintf(buffer, maxLogBuf, fmt, ap);
328328
va_end(ap);
329329

330+
// Ensure buffer is null-terminated even if vsnprintf failed or truncated.
331+
if (r0 < 0) {
332+
buffer[0] = '\0';
333+
} else if (r0 >= maxLogBuf) {
334+
buffer[maxLogBuf - 1] = '\0';
335+
}
336+
330337
SrsCplxError *err = new SrsCplxError();
331338

332339
err->func_ = func;
333340
err->file_ = file;
334341
err->line_ = line;
335-
if (v) {
336-
err->code_ = v->code_;
337-
}
342+
err->code_ = srs_error_code(v);
338343
err->rerrno_ = rerrno;
339-
if (r0 > 0 && r0 < maxLogBuf) {
340-
err->msg_ = string(buffer, r0);
341-
}
344+
err->msg_ = string(buffer);
342345
err->wrapped_ = v;
343346
if (_srs_context) {
344347
err->cid_ = _srs_context->get_id();
@@ -347,6 +350,29 @@ SrsCplxError *SrsCplxError::wrap(const char *func, const char *file, int line, S
347350
return err;
348351
}
349352

353+
SrsCplxError *SrsCplxError::transform(const char *func, const char *file, int line, int code, SrsCplxError *v, const char *fmt, ...)
354+
{
355+
va_list ap;
356+
va_start(ap, fmt);
357+
static char *buffer = new char[maxLogBuf];
358+
int r0 = vsnprintf(buffer, maxLogBuf, fmt, ap);
359+
va_end(ap);
360+
361+
// Ensure buffer is null-terminated even if vsnprintf failed or truncated.
362+
if (r0 < 0) {
363+
buffer[0] = '\0';
364+
} else if (r0 >= maxLogBuf) {
365+
buffer[maxLogBuf - 1] = '\0';
366+
}
367+
368+
// Wrap the error with additional context showing the code transformation.
369+
SrsCplxError *err = NULL;
370+
err = wrap(func, file, line, v, "code transformed (%d => %d): %s", srs_error_code(v), code, buffer);
371+
err->code_ = code;
372+
373+
return err;
374+
}
375+
350376
SrsCplxError *SrsCplxError::success()
351377
{
352378
return NULL;

trunk/src/kernel/srs_kernel_error.hpp

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@
111111
XX(ERROR_STREAM_DISPOSING, 1098, "StreamDisposing", "Stream is disposing") \
112112
XX(ERROR_NOT_IMPLEMENTED, 1099, "NotImplemented", "Feature is not implemented") \
113113
XX(ERROR_NOT_SUPPORTED, 1100, "NotSupported", "Feature is not supported") \
114-
XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file")
114+
XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file") \
115+
XX(ERROR_SYSTEM_AUTH, 1102, "SystemAuth", "Failed to authenticate stream")
116+
115117

116118
/**************************************************/
117119
/* RTMP protocol error. */
@@ -380,7 +382,8 @@
380382
XX(ERROR_RTSP_NO_TRACK, 5039, "RtspNoTrack", "Drop RTSP packet for track not found") \
381383
XX(ERROR_RTSP_TOKEN_NOT_NORMAL, 5040, "RtspToken", "Invalid RTSP token state not normal") \
382384
XX(ERROR_RTSP_REQUEST_HEADER_EOF, 5041, "RtspHeaderEof", "Invalid RTSP request for header EOF") \
383-
XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing")
385+
XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") \
386+
XX(ERROR_RTC_INVALID_SDP, 5043, "RtcInvalidSdp", "Invalid SDP for RTC")
384387

385388
/**************************************************/
386389
/* SRT protocol error. */
@@ -465,6 +468,7 @@ class SrsCplxError
465468
public:
466469
static SrsCplxError *create(const char *func, const char *file, int line, int code, const char *fmt, ...);
467470
static SrsCplxError *wrap(const char *func, const char *file, int line, SrsCplxError *err, const char *fmt, ...);
471+
static SrsCplxError *transform(const char *func, const char *file, int line, int code, SrsCplxError *err, const char *fmt, ...);
468472
static SrsCplxError *success();
469473
static SrsCplxError *copy(SrsCplxError *from);
470474
static std::string description(SrsCplxError *err);
@@ -479,13 +483,73 @@ class SrsCplxError
479483

480484
// Error helpers, should use these functions to new or wrap an error.
481485
#define srs_success NULL // SrsCplxError::success()
482-
#define srs_error_new(ret, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, ret, fmt, ##__VA_ARGS__)
486+
487+
// Create a new error with the specified error code and message.
488+
//
489+
// Example:
490+
// if (fd < 0) {
491+
// return srs_error_new(ERROR_SOCKET_CREATE, "create socket fd=%d", fd);
492+
// }
493+
#define srs_error_new(code, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__)
494+
495+
// Wrap an existing error with additional context. The error code is
496+
// preserved from the wrapped error.
497+
//
498+
// Example:
499+
// if ((err = do_connect(host, port)) != srs_success) {
500+
// return srs_error_wrap(err, "connect to %s:%d", host.c_str(), port);
501+
// }
483502
#define srs_error_wrap(err, fmt, ...) SrsCplxError::wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__)
503+
504+
// Transform an error by wrapping it and changing its error code. Useful
505+
// for converting internal errors to protocol-specific error codes (e.g.,
506+
// HTTP, RTMP). The wrapped error chain is preserved.
507+
//
508+
// Example:
509+
// if ((err = http_hooks_on_publish(ruc->req_)) != srs_success) {
510+
// return srs_error_transform(ERROR_SYSTEM_AUTH, err, "RTC: http_hooks_on_publish");
511+
// }
512+
#define srs_error_transform(code, err, fmt, ...) SrsCplxError::transform(__FUNCTION__, __FILE__, __LINE__, code, err, fmt, ##__VA_ARGS__)
513+
514+
// Copy an error object. Returns a new error object with the same content.
515+
//
516+
// Example:
517+
// srs_error_t err_copy = srs_error_copy(err);
484518
#define srs_error_copy(err) SrsCplxError::copy(err)
519+
520+
// Get the full description of an error including the entire error chain.
521+
//
522+
// Example:
523+
// srs_error_t err = do_something();
524+
// srs_warn("error: %s", srs_error_desc(err).c_str());
485525
#define srs_error_desc(err) SrsCplxError::description(err)
526+
527+
// Get a brief summary of an error (only the top-level message).
528+
//
529+
// Example:
530+
// srs_error_t err = do_something();
531+
// srs_trace("error summary: %s", srs_error_summary(err).c_str());
486532
#define srs_error_summary(err) SrsCplxError::summary(err)
533+
534+
// Get the error code as an integer.
535+
//
536+
// Example:
537+
// int code = srs_error_code(err);
538+
// if (code == ERROR_SOCKET_TIMEOUT) { ... }
487539
#define srs_error_code(err) SrsCplxError::error_code(err)
540+
541+
// Get the error code as a short string (e.g., "SocketTimeout").
542+
//
543+
// Example:
544+
// string code_str = srs_error_code_str(err);
545+
// srs_trace("error code: %s", code_str.c_str());
488546
#define srs_error_code_str(err) SrsCplxError::error_code_str(err)
547+
548+
// Get the error code description (e.g., "Socket io timeout").
549+
//
550+
// Example:
551+
// string desc = srs_error_code_strlong(err);
552+
// srs_warn("error description: %s", desc.c_str());
489553
#define srs_error_code_strlong(err) SrsCplxError::error_code_strlong(err)
490554

491555
#ifndef srs_assert

trunk/src/utest/srs_utest_ai05.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,74 @@ VOID TEST(KernelErrorTest, ErrorChaining)
11421142
srs_freep(level3);
11431143
}
11441144

1145+
VOID TEST(KernelErrorTest, SrsCplxErrorTransform)
1146+
{
1147+
srs_error_t err;
1148+
1149+
// Test transform with real error - changes error code
1150+
srs_error_t original = srs_error_new(ERROR_SYSTEM_STREAM_BUSY, "stream busy");
1151+
err = srs_error_transform(ERROR_SYSTEM_AUTH, original, "authentication failed");
1152+
1153+
EXPECT_TRUE(err != srs_success);
1154+
EXPECT_EQ(ERROR_SYSTEM_AUTH, srs_error_code(err)); // Should have new code
1155+
1156+
// Check description contains both messages and code transformation info
1157+
std::string desc = srs_error_desc(err);
1158+
EXPECT_TRUE(desc.find("authentication failed") != std::string::npos);
1159+
EXPECT_TRUE(desc.find("stream busy") != std::string::npos);
1160+
EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
1161+
EXPECT_TRUE(desc.find("1028") != std::string::npos); // Original code ERROR_SYSTEM_STREAM_BUSY
1162+
EXPECT_TRUE(desc.find("1102") != std::string::npos); // New code ERROR_SYSTEM_AUTH
1163+
1164+
srs_freep(err);
1165+
1166+
// Test transform with NULL error
1167+
err = srs_error_transform(ERROR_HTTP_STATUS_INVALID, srs_success, "transform null error");
1168+
EXPECT_TRUE(err != srs_success);
1169+
EXPECT_EQ(ERROR_HTTP_STATUS_INVALID, srs_error_code(err)); // Should have specified code
1170+
1171+
desc = srs_error_desc(err);
1172+
EXPECT_TRUE(desc.find("transform null error") != std::string::npos);
1173+
EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
1174+
1175+
srs_freep(err);
1176+
1177+
// Test transform with formatted message
1178+
srs_error_t inner = srs_error_new(ERROR_RTC_SDP_DECODE, "invalid sdp format");
1179+
err = srs_error_transform(ERROR_HTTP_STATUS_INVALID, inner, "http error: %s", "bad request");
1180+
1181+
EXPECT_TRUE(err != srs_success);
1182+
EXPECT_EQ(ERROR_HTTP_STATUS_INVALID, srs_error_code(err));
1183+
1184+
desc = srs_error_desc(err);
1185+
EXPECT_TRUE(desc.find("http error: bad request") != std::string::npos);
1186+
EXPECT_TRUE(desc.find("invalid sdp format") != std::string::npos);
1187+
EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
1188+
// Verify the code transformation shows both old and new codes
1189+
std::string code_transform_msg = srs_fmt_sprintf("(%d => %d)", ERROR_RTC_SDP_DECODE, ERROR_HTTP_STATUS_INVALID);
1190+
EXPECT_TRUE(desc.find(code_transform_msg) != std::string::npos);
1191+
1192+
srs_freep(err);
1193+
1194+
// Test chaining: wrap -> transform -> wrap
1195+
srs_error_t level1 = srs_error_new(ERROR_SOCKET_READ, "socket read failed");
1196+
srs_error_t level2 = srs_error_wrap(level1, "connection error");
1197+
srs_error_t level3 = srs_error_transform(ERROR_SYSTEM_AUTH, level2, "auth check failed");
1198+
srs_error_t level4 = srs_error_wrap(level3, "publish rejected");
1199+
1200+
EXPECT_TRUE(level4 != srs_success);
1201+
EXPECT_EQ(ERROR_SYSTEM_AUTH, srs_error_code(level4)); // Should have transformed code
1202+
1203+
desc = srs_error_desc(level4);
1204+
EXPECT_TRUE(desc.find("socket read failed") != std::string::npos);
1205+
EXPECT_TRUE(desc.find("connection error") != std::string::npos);
1206+
EXPECT_TRUE(desc.find("auth check failed") != std::string::npos);
1207+
EXPECT_TRUE(desc.find("publish rejected") != std::string::npos);
1208+
EXPECT_TRUE(desc.find("code transformed") != std::string::npos);
1209+
1210+
srs_freep(level4);
1211+
}
1212+
11451213
VOID TEST(KernelErrorTest, ErrorDescriptionFormatting)
11461214
{
11471215
srs_error_t err;

0 commit comments

Comments
 (0)