Repository: bluenviron/mediamtx

Issue:
Improve Error Handling for JWKS Misconfiguration/Unreachability
### Describe the feature

When Mediamtx is configured to use JWT authentication and encounters an issue fetching or parsing the JWKS due to misconfiguration (e.g., incorrect URI) or unreachability (e.g., network issue, JWKS server down), it currently responds with a generic `401 Unauthorized` HTTP status code. Furthermore, even with loglevel debug there are no specific logs for what the issue is.

This behavior makes debugging significantly difficult as the `401` status doesn't differentiate between various authorization failures (e.g., invalid token, expired token, Mediamtx's inability to validate due to JWKS issues). Administrators are left to guess the root cause, leading to increased troubleshooting time.

### logs
```
2025-06-11 13:02:29 2025/06/11 05:02:29 DEB [RTSP] [conn 172.23.0.3:56590] [c->s] ANNOUNCE rtsp://mediamtx:8554/1?jwt=****** RTSP/1.0
2025-06-11 13:02:29 CSeq: 2
2025-06-11 13:02:29 Content-Length: 317
2025-06-11 13:02:29 Content-Type: application/sdp
2025-06-11 13:02:29 User-Agent: Lavf60.16.100
2025-06-11 13:02:29 
2025-06-11 13:02:29 v=0
2025-06-11 13:02:29 o=- 0 0 IN IP4 127.0.0.1
2025-06-11 13:02:29 s=bensound-rebelsofourownkind
2025-06-11 13:02:29 c=IN IP4 172.23.0.2
2025-06-11 13:02:29 t=0 0
2025-06-11 13:02:29 a=tool:libavformat 60.16.100
2025-06-11 13:02:29 m=audio 0 RTP/AVP 96
2025-06-11 13:02:29 b=AS:69
2025-06-11 13:02:29 a=rtpmap:96 MPEG4-GENERIC/16000/1
2025-06-11 13:02:29 a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=140856E500
2025-06-11 13:02:29 a=control:streamid=0
2025-06-11 13:02:29 
2025-06-11 13:02:29 2025/06/11 05:02:29 INF [RTSP] [session 9123b30b] created by 172.23.0.3:56590
2025-06-11 13:02:29 2025/06/11 05:02:29 DEB [RTSP] [conn 172.23.0.3:56590] [s->c] RTSP/1.0 401 Unauthorized
2025-06-11 13:02:29 CSeq: 2
2025-06-11 13:02:29 Server: gortsplib
2025-06-11 13:02:29 WWW-Authenticate: Basic realm="ipcam"
```
Mediamtx should provide more specific error feedback when JWKS issues prevent token validation. This could involve:

1.  **More specific HTTP status codes:**
    e.g.  `50x Internal Server Error` 
3.  **Enhanced server-side logging:** more detailed logging about the JWKS retrieval/parsing failure or if JWKS server unreachable.

Hints from issue comments:
@optical: Agree with this, I've spent a bit of time chasing down 401's with JWT auth, and am currently trying to diagnose sporadic 401's being returned@aler9: Hello, i checked log messages related to JWT authentication failures and this is the current situation:

**RTMP, SRT, HLS, WebRTC, Playback**

Both server errors and user-related errors are reported and can be clearly distinguished:

```
2025/09/22 10:44:53 INF [RTMP] [conn 127.0.0.1:56936] closed: authentication failed: Get "http://loca1lhost:8080/realms/mediamtx/protocol/openid-connect/certs": dial tcp: lookup loca1lhost on 127.0.0.53:53: server misbehaving
2025/09/22 10:46:04 INF [RTMP] [conn 127.0.0.1:44276] closed: authentication failed: token is malformed: token contains an invalid number of segments
2025/09/22 11:18:55 INF [HLS] connection 127.0.0.1:55734 failed to authenticate: token is malformed: token contains an invalid number of segments
2025/09/22 11:19:12 INF [WebRTC] connection 127.0.0.1:51988 failed to authenticate: token is malformed: token contains an invalid number of segments
2025/09/22 11:21:43 INF [playback] connection 127.0.0.1:40354 failed to authenticate: token is malformed: token contains an invalid number of segments
```

Keep in mind that in case of HTTP-based protocols (HLS, WebRTC), it's normal for the server to return a 401 response without any log line being printed, since that response is needed to ask users for credentials.

**API, Metrics, pprof**

Errors are not logged.

**RTSP**

errors cannot be comprehended:

```
2025/09/22 10:51:18 INF [RTSP] [conn 127.0.0.1:51150] closed: authentication error
```

Therefore: the server will be improved accordingly.@github-actions: This issue is mentioned in release v1.15.1 🚀
Check out the entire changelog by [clicking here](https://github.com/bluenviron/mediamtx/releases/tag/v1.15.1)

Solution patch:
diff --git a/internal/api/api.go b/internal/api/api.go
index 1c9a546b..181a10be 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -260,7 +260,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
 
 		a.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
 
-		// wait some seconds to mitigate brute force attacks
+		// wait some seconds to delay brute force attacks
 		<-time.After(auth.PauseAfterError)
 
 		ctx.AbortWithStatus(http.StatusUnauthorized)
diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go
index 2b785488..c1735792 100644
--- a/internal/metrics/metrics.go
+++ b/internal/metrics/metrics.go
@@ -164,7 +164,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
 
 		m.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
 
-		// wait some seconds to mitigate brute force attacks
+		// wait some seconds to delay brute force attacks
 		<-time.After(auth.PauseAfterError)
 
 		ctx.AbortWithStatus(http.StatusUnauthorized)
diff --git a/internal/playback/server.go b/internal/playback/server.go
index d67330d7..a3f11fc6 100644
--- a/internal/playback/server.go
+++ b/internal/playback/server.go
@@ -132,7 +132,7 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
 		s.Log(logger.Info, "connection %v failed to authenticate: %v",
 			httpp.RemoteAddr(ctx), err.Wrapped)
 
-		// wait some seconds to mitigate brute force attacks
+		// wait some seconds to delay brute force attacks
 		<-time.After(auth.PauseAfterError)
 
 		ctx.Writer.WriteHeader(http.StatusUnauthorized)
diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go
index 12d65173..90dac014 100644
--- a/internal/pprof/pprof.go
+++ b/internal/pprof/pprof.go
@@ -110,7 +110,7 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
 
 		pp.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
 
-		// wait some seconds to mitigate brute force attacks
+		// wait some seconds to delay brute force attacks
 		<-time.After(auth.PauseAfterError)
 
 		ctx.AbortWithStatus(http.StatusUnauthorized)
diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go
index fb053a16..2dd5ccba 100644
--- a/internal/servers/hls/http_server.go
+++ b/internal/servers/hls/http_server.go
@@ -164,7 +164,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
 
 			s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
 
-			// wait some seconds to mitigate brute force attacks
+			// wait some seconds to delay brute force attacks
 			<-time.After(auth.PauseAfterError)
 
 			ctx.Writer.WriteHeader(http.StatusUnauthorized)
diff --git a/internal/servers/rtmp/conn.go b/internal/servers/rtmp/conn.go
index f710d92b..9d511d76 100644
--- a/internal/servers/rtmp/conn.go
+++ b/internal/servers/rtmp/conn.go
@@ -168,7 +168,7 @@ func (c *conn) runRead() error {
 	if err != nil {
 		var terr *auth.Error
 		if errors.As(err, &terr) {
-			// wait some seconds to mitigate brute force attacks
+			// wait some seconds to delay brute force attacks
 			<-time.After(auth.PauseAfterError)
 			return terr
 		}
@@ -256,7 +256,7 @@ func (c *conn) runPublish() error {
 	if err != nil {
 		var terr *auth.Error
 		if errors.As(err, &terr) {
-			// wait some seconds to mitigate brute force attacks
+			// wait some seconds to delay brute force attacks
 			<-time.After(auth.PauseAfterError)
 			return terr
 		}
diff --git a/internal/servers/rtsp/conn.go b/internal/servers/rtsp/conn.go
index 550762a2..9d3e51e4 100644
--- a/internal/servers/rtsp/conn.go
+++ b/internal/servers/rtsp/conn.go
@@ -11,7 +11,6 @@ import (
 	"github.com/bluenviron/gortsplib/v5"
 	rtspauth "github.com/bluenviron/gortsplib/v5/pkg/auth"
 	"github.com/bluenviron/gortsplib/v5/pkg/base"
-	"github.com/bluenviron/gortsplib/v5/pkg/headers"
 	"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
 	"github.com/google/uuid"
 
@@ -37,12 +36,6 @@ func absoluteURL(req *base.Request, v string) string {
 	return v
 }
 
-func credentialsProvided(req *base.Request) bool {
-	var auth headers.Authorization
-	err := auth.Unmarshal(req.Header["Authorization"])
-	return err == nil && auth.Username != ""
-}
-
 func tunnelLabel(t gortsplib.Tunnel) string {
 	switch t {
 	case gortsplib.TunnelHTTP:
@@ -175,7 +168,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
 	if res.Err != nil {
 		var terr *auth.Error
 		if errors.As(res.Err, &terr) {
-			res, err2 := c.handleAuthError(ctx.Request)
+			res, err2 := c.handleAuthError(terr)
 			return res, nil, err2
 		}
 
@@ -212,17 +205,19 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
 	}, stream, nil
 }
 
-func (c *conn) handleAuthError(req *base.Request) (*base.Response, error) {
-	if credentialsProvided(req) {
-		// wait some seconds to mitigate brute force attacks
-		<-time.After(auth.PauseAfterError)
+func (c *conn) handleAuthError(err *auth.Error) (*base.Response, error) {
+	if err.AskCredentials {
+		return &base.Response{
+			StatusCode: base.StatusUnauthorized,
+		}, liberrors.ErrServerAuth{}
 	}
 
-	// let gortsplib decide whether connection should be terminated,
-	// depending on whether credentials have been provided or not.
+	// wait some seconds to delay brute force attacks
+	<-time.After(auth.PauseAfterError)
+
 	return &base.Response{
 		StatusCode: base.StatusUnauthorized,
-	}, liberrors.ErrServerAuth{}
+	}, err
 }
 
 func (c *conn) apiItem() *defs.APIRTSPConn {
diff --git a/internal/servers/rtsp/session.go b/internal/servers/rtsp/session.go
index 2ccda113..03f2376a 100644
--- a/internal/servers/rtsp/session.go
+++ b/internal/servers/rtsp/session.go
@@ -179,7 +179,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
 	if err != nil {
 		var terr *auth.Error
 		if errors.As(err, &terr) {
-			return c.handleAuthError(ctx.Request)
+			return c.handleAuthError(terr)
 		}
 
 		return &base.Response{
@@ -242,7 +242,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
 		if err != nil {
 			var terr *auth.Error
 			if errors.As(err, &terr) {
-				res, err2 := c.handleAuthError(ctx.Request)
+				res, err2 := c.handleAuthError(terr)
 				return res, nil, err2
 			}
 
diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go
index f9130ef1..a5498eba 100644
--- a/internal/servers/srt/conn.go
+++ b/internal/servers/srt/conn.go
@@ -148,7 +148,7 @@ func (c *conn) runPublish(streamID *streamID) error {
 	if err != nil {
 		var terr *auth.Error
 		if errors.As(err, &terr) {
-			// wait some seconds to mitigate brute force attacks
+			// wait some seconds to delay brute force attacks
 			<-time.After(auth.PauseAfterError)
 			c.connReq.Reject(srt.REJ_PEER)
 			return terr
@@ -272,7 +272,7 @@ func (c *conn) runRead(streamID *streamID) error {
 	if err != nil {
 		var terr *auth.Error
 		if errors.As(err, &terr) {
-			// wait some seconds to mitigate brute force attacks
+			// wait some seconds to delay brute force attacks
 			<-time.After(auth.PauseAfterError)
 			c.connReq.Reject(srt.REJ_PEER)
 			return terr
diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go
index ee22f2f5..ca96253b 100644
--- a/internal/servers/webrtc/http_server.go
+++ b/internal/servers/webrtc/http_server.go
@@ -141,7 +141,7 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string,
 
 			s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
 
-			// wait some seconds to mitigate brute force attacks
+			// wait some seconds to delay brute force attacks
 			<-time.After(auth.PauseAfterError)
 
 			writeError(ctx, http.StatusUnauthorized, terr)
@@ -203,7 +203,7 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
 
 			s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
 
-			// wait some seconds to mitigate brute force attacks
+			// wait some seconds to delay brute force attacks
 			<-time.After(auth.PauseAfterError)
 
 			writeError(ctx, http.StatusUnauthorized, terr)



Test patch:
diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go
index 1244616d..75bd0e72 100644
--- a/internal/servers/hls/server_test.go
+++ b/internal/servers/hls/server_test.go
@@ -13,9 +13,11 @@ import (
 	"github.com/bluenviron/gohlslib/v2/pkg/codecs"
 	"github.com/bluenviron/gortsplib/v5/pkg/description"
 	"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
+	"github.com/bluenviron/mediamtx/internal/auth"
 	"github.com/bluenviron/mediamtx/internal/conf"
 	"github.com/bluenviron/mediamtx/internal/defs"
 	"github.com/bluenviron/mediamtx/internal/externalcmd"
+	"github.com/bluenviron/mediamtx/internal/logger"
 	"github.com/bluenviron/mediamtx/internal/stream"
 	"github.com/bluenviron/mediamtx/internal/test"
 	"github.com/bluenviron/mediamtx/internal/unit"
@@ -500,3 +502,66 @@ func TestServerDynamicAlwaysRemux(t *testing.T) {
 
 	<-done
 }
+
+func TestAuthError(t *testing.T) {
+	n := 0
+
+	s := &Server{
+		Address:         "127.0.0.1:8888",
+		Encryption:      false,
+		ServerKey:       "",
+		ServerCert:      "",
+		AlwaysRemux:     true,
+		Variant:         conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
+		SegmentCount:    7,
+		SegmentDuration: conf.Duration(1 * time.Second),
+		PartDuration:    conf.Duration(200 * time.Millisecond),
+		SegmentMaxSize:  50 * 1024 * 1024,
+		ReadTimeout:     conf.Duration(10 * time.Second),
+		PathManager: &dummyPathManager{
+			findPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
+				if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
+					return nil, &auth.Error{AskCredentials: true}
+				}
+
+				return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
+			},
+		},
+		Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
+			if l == logger.Info {
+				if n == 1 {
+					require.Regexp(t, "failed to authenticate: auth error$", fmt.Sprintf(s, i...))
+				}
+				n++
+			}
+		}),
+	}
+	err := s.Initialize()
+	require.NoError(t, err)
+	defer s.Close()
+
+	req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/stream/index.m3u8", nil)
+	require.NoError(t, err)
+
+	res, err := http.DefaultClient.Do(req)
+	require.NoError(t, err)
+	defer res.Body.Close()
+
+	require.Equal(t, http.StatusUnauthorized, res.StatusCode)
+	require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate"))
+
+	req, err = http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8888/stream/index.m3u8", nil)
+	require.NoError(t, err)
+
+	start := time.Now()
+
+	res, err = http.DefaultClient.Do(req)
+	require.NoError(t, err)
+	defer res.Body.Close()
+
+	require.Greater(t, time.Since(start), 2*time.Second)
+
+	require.Equal(t, http.StatusUnauthorized, res.StatusCode)
+
+	require.Equal(t, 2, n)
+}
diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go
index cb7fac3c..fded66bc 100644
--- a/internal/servers/rtsp/server_test.go
+++ b/internal/servers/rtsp/server_test.go
@@ -1,6 +1,8 @@
 package rtsp
 
 import (
+	"fmt"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -13,6 +15,7 @@ import (
 	"github.com/bluenviron/mediamtx/internal/conf"
 	"github.com/bluenviron/mediamtx/internal/defs"
 	"github.com/bluenviron/mediamtx/internal/externalcmd"
+	"github.com/bluenviron/mediamtx/internal/logger"
 	"github.com/bluenviron/mediamtx/internal/stream"
 	"github.com/bluenviron/mediamtx/internal/test"
 	"github.com/bluenviron/mediamtx/internal/unit"
@@ -393,3 +396,54 @@ func TestServerRedirect(t *testing.T) {
 		})
 	}
 }
+
+func TestAuthError(t *testing.T) {
+	pathManager := &test.PathManager{
+		DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
+			if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
+				return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true}}
+			}
+
+			return defs.PathDescribeRes{Err: &auth.Error{Wrapped: fmt.Errorf("auth error")}}
+		},
+	}
+
+	n := new(int64)
+	done := make(chan struct{})
+
+	s := &Server{
+		Address:        "127.0.0.1:8557",
+		ReadTimeout:    conf.Duration(10 * time.Second),
+		WriteTimeout:   conf.Duration(10 * time.Second),
+		WriteQueueSize: 512,
+		PathManager:    pathManager,
+		Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
+			if l == logger.Info {
+				if atomic.AddInt64(n, 1) == 3 {
+					require.Regexp(t, "authentication failed: auth error$", fmt.Sprintf(s, i...))
+					close(done)
+				}
+			}
+		}),
+	}
+	err := s.Initialize()
+	require.NoError(t, err)
+	defer s.Close()
+
+	u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value")
+	require.NoError(t, err)
+
+	reader := gortsplib.Client{
+		Scheme: u.Scheme,
+		Host:   u.Host,
+	}
+
+	err = reader.Start()
+	require.NoError(t, err)
+	defer reader.Close()
+
+	_, _, err = reader.Describe(u)
+	require.EqualError(t, err, "bad status code: 401 (Unauthorized)")
+
+	<-done
+}
diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go
index 3e3b5f8a..ed59bdd1 100644
--- a/internal/servers/webrtc/server_test.go
+++ b/internal/servers/webrtc/server_test.go
@@ -3,6 +3,7 @@ package webrtc
 import (
 	"bytes"
 	"context"
+	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -12,9 +13,11 @@ import (
 
 	"github.com/bluenviron/gortsplib/v5/pkg/description"
 	"github.com/bluenviron/gortsplib/v5/pkg/format"
+	"github.com/bluenviron/mediamtx/internal/auth"
 	"github.com/bluenviron/mediamtx/internal/conf"
 	"github.com/bluenviron/mediamtx/internal/defs"
 	"github.com/bluenviron/mediamtx/internal/externalcmd"
+	"github.com/bluenviron/mediamtx/internal/logger"
 	"github.com/bluenviron/mediamtx/internal/protocols/webrtc"
 	"github.com/bluenviron/mediamtx/internal/protocols/whip"
 	"github.com/bluenviron/mediamtx/internal/stream"
@@ -63,9 +66,6 @@ func initializeTestServer(t *testing.T) *Server {
 
 	s := &Server{
 		Address:               "127.0.0.1:8886",
-		Encryption:            false,
-		ServerKey:             "",
-		ServerCert:            "",
 		AllowOrigin:           "*",
 		TrustedProxies:        conf.IPNetworks{},
 		ReadTimeout:           conf.Duration(10 * time.Second),
@@ -78,7 +78,6 @@ func initializeTestServer(t *testing.T) *Server {
 		HandshakeTimeout:      conf.Duration(10 * time.Second),
 		TrackGatherTimeout:    conf.Duration(2 * time.Second),
 		STUNGatherTimeout:     conf.Duration(5 * time.Second),
-		ExternalCmdPool:       nil,
 		PathManager:           pm,
 		Parent:                test.NilLogger,
 	}
@@ -148,10 +147,6 @@ func TestServerOptionsICEServer(t *testing.T) {
 
 	s := &Server{
 		Address:               "127.0.0.1:8886",
-		Encryption:            false,
-		ServerKey:             "",
-		ServerCert:            "",
-		AllowOrigin:           "",
 		TrustedProxies:        conf.IPNetworks{},
 		ReadTimeout:           conf.Duration(10 * time.Second),
 		LocalUDPAddress:       "127.0.0.1:8887",
@@ -167,7 +162,6 @@ func TestServerOptionsICEServer(t *testing.T) {
 		HandshakeTimeout:   conf.Duration(10 * time.Second),
 		TrackGatherTimeout: conf.Duration(2 * time.Second),
 		STUNGatherTimeout:  conf.Duration(5 * time.Second),
-		ExternalCmdPool:    nil,
 		PathManager:        pathManager,
 		Parent:             test.NilLogger,
 	}
@@ -234,10 +228,6 @@ func TestServerPublish(t *testing.T) {
 
 	s := &Server{
 		Address:               "127.0.0.1:8886",
-		Encryption:            false,
-		ServerKey:             "",
-		ServerCert:            "",
-		AllowOrigin:           "",
 		TrustedProxies:        conf.IPNetworks{},
 		ReadTimeout:           conf.Duration(10 * time.Second),
 		LocalUDPAddress:       "127.0.0.1:8887",
@@ -249,7 +239,6 @@ func TestServerPublish(t *testing.T) {
 		HandshakeTimeout:      conf.Duration(10 * time.Second),
 		TrackGatherTimeout:    conf.Duration(2 * time.Second),
 		STUNGatherTimeout:     conf.Duration(5 * time.Second),
-		ExternalCmdPool:       nil,
 		PathManager:           pathManager,
 		Parent:                test.NilLogger,
 	}
@@ -523,11 +512,6 @@ func TestServerRead(t *testing.T) {
 
 			s := &Server{
 				Address:               "127.0.0.1:8886",
-				Encryption:            false,
-				ServerKey:             "",
-				ServerCert:            "",
-				AllowOrigin:           "",
-				TrustedProxies:        conf.IPNetworks{},
 				ReadTimeout:           conf.Duration(10 * time.Second),
 				LocalUDPAddress:       "127.0.0.1:8887",
 				LocalTCPAddress:       "127.0.0.1:8887",
@@ -538,7 +522,6 @@ func TestServerRead(t *testing.T) {
 				HandshakeTimeout:      conf.Duration(10 * time.Second),
 				TrackGatherTimeout:    conf.Duration(2 * time.Second),
 				STUNGatherTimeout:     conf.Duration(5 * time.Second),
-				ExternalCmdPool:       nil,
 				PathManager:           pathManager,
 				Parent:                test.NilLogger,
 			}
@@ -612,10 +595,6 @@ func TestServerReadNotFound(t *testing.T) {
 
 	s := &Server{
 		Address:               "127.0.0.1:8886",
-		Encryption:            false,
-		ServerKey:             "",
-		ServerCert:            "",
-		AllowOrigin:           "",
 		TrustedProxies:        conf.IPNetworks{},
 		ReadTimeout:           conf.Duration(10 * time.Second),
 		LocalUDPAddress:       "127.0.0.1:8887",
@@ -627,7 +606,6 @@ func TestServerReadNotFound(t *testing.T) {
 		HandshakeTimeout:      conf.Duration(10 * time.Second),
 		TrackGatherTimeout:    conf.Duration(2 * time.Second),
 		STUNGatherTimeout:     conf.Duration(5 * time.Second),
-		ExternalCmdPool:       nil,
 		PathManager:           pm,
 		Parent:                test.NilLogger,
 	}
@@ -754,3 +732,57 @@ func TestICEServerClientOnly(t *testing.T) {
 	require.NoError(t, err)
 	require.Empty(t, serverICEServers)
 }
+
+func TestAuthError(t *testing.T) {
+	n := 0
+
+	s := &Server{
+		Address:     "127.0.0.1:8886",
+		ReadTimeout: conf.Duration(10 * time.Second),
+		PathManager: &test.PathManager{
+			FindPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
+				if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
+					return nil, &auth.Error{AskCredentials: true}
+				}
+
+				return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
+			},
+		},
+		Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
+			if l == logger.Info {
+				if n == 1 {
+					require.Regexp(t, "failed to authenticate: auth error$", fmt.Sprintf(s, i...))
+				}
+				n++
+			}
+		}),
+	}
+	err := s.Initialize()
+	require.NoError(t, err)
+	defer s.Close()
+
+	req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8886/stream/publish", nil)
+	require.NoError(t, err)
+
+	res, err := http.DefaultClient.Do(req)
+	require.NoError(t, err)
+	defer res.Body.Close()
+
+	require.Equal(t, http.StatusUnauthorized, res.StatusCode)
+	require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate"))
+
+	req, err = http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8886/stream/publish", nil)
+	require.NoError(t, err)
+
+	start := time.Now()
+
+	res, err = http.DefaultClient.Do(req)
+	require.NoError(t, err)
+	defer res.Body.Close()
+
+	require.Greater(t, time.Since(start), 2*time.Second)
+
+	require.Equal(t, http.StatusUnauthorized, res.StatusCode)
+
+	require.Equal(t, 2, n)
+}