WebDAV: Route request errors to the system log, not the UI #5715

All WebDAV write-method errors (PUT/DELETE/MOVE/COPY/MKCOL/...) now go to
the console-only system log instead of the in-app errors table and browser
log viewer, because golang.org/x/net/webdav embeds absolute server paths in
its messages. Operators still see full detail in the container/service logs.

A MKCOL against an already-existing collection is a benign probe used by
sync clients such as PhotoSync to test for a directory before creating it;
it returns 405 and is logged at debug rather than as an error.
This commit is contained in:
Michael Mayer 2026-07-07 23:27:52 +00:00
parent d33bca4f48
commit 7bea2dccfe
3 changed files with 70 additions and 6 deletions

View file

@ -57,6 +57,7 @@
- WebDAV response behavior:
- Built-in security middleware skips browser-document headers (`Content-Security-Policy`, `X-Frame-Options`) on `/originals` and `/import` paths.
- PROPFIND `207 Multi-Status` responses normalize XML media type to `application/xml; charset=utf-8`.
- Request errors go to the console-only system log (`event.System*`), not the browser log stream, since `x/net/webdav` embeds absolute server paths in its messages. A `MKCOL` on an existing collection is a benign sync-client probe: it returns 405 and is logged at debug rather than as an error.
- AutoTLS: uses `autocert` and spins up a redirect listener; ensure ports 80/443 are reachable.
- Unix sockets: optional `force` query removes stale sockets; permissions can be set via `mode` query.
- Health endpoints (`/livez`, `/health`, `/healthz`, `/readyz`) return `Cache-Control: no-store` and `Access-Control-Allow-Origin: *`.

View file

@ -1,6 +1,7 @@
package server
import (
"errors"
"fmt"
"net/http"
"os"
@ -13,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/workers/auto"
"github.com/photoprism/photoprism/pkg/clean"
@ -55,13 +57,19 @@ func WebDAV(dir string, router *gin.RouterGroup, conf *config.Config) {
// Request logger function.
loggerFunc := func(request *http.Request, err error) {
if err != nil {
switch request.Method {
case header.MethodPut, header.MethodMkcol, header.MethodDelete, header.MethodMove, header.MethodCopy, header.MethodProppatch, header.MethodLock, header.MethodUnlock:
log.Errorf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
case header.MethodPropfind:
log.Tracef("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
// Route WebDAV request errors to the console-only system log, not log.*.
// x/net/webdav embeds absolute originals/import paths in its messages,
// which must stay out of the browser log viewer and the persisted errors
// table; operators still see full detail in the server console.
switch {
case request.Method == header.MethodMkcol && errors.Is(err, os.ErrExist):
// MKCOL on an existing collection is a benign probe: sync clients such as
// PhotoSync test for a directory before creating it — expected, not a failure.
event.SystemDebug([]string{"webdav", "collection %s already exists"}, clean.Log(request.URL.String()))
case WebDAVWriteMethod(request.Method):
event.SystemError([]string{"webdav", "%s in %s %s"}, clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
default:
log.Debugf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
event.SystemDebug([]string{"webdav", "%s in %s %s"}, clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
}
} else {
// Determine the filename if it is an uploaded file and process custom request headers, if any.

View file

@ -11,10 +11,12 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/http/header"
)
@ -64,6 +66,59 @@ func TestWebDAVWrite_MKCOL_PUT(t *testing.T) {
assert.Equal(t, "hello", string(b))
}
// logCapture is a logrus hook that records emitted entries for assertions.
type logCapture struct{ entries []*logrus.Entry }
// Levels reports the log levels the capture hook fires on.
func (h *logCapture) Levels() []logrus.Level { return logrus.AllLevels }
// Fire records the given log entry.
func (h *logCapture) Fire(e *logrus.Entry) error {
h.entries = append(h.entries, e)
return nil
}
func TestWebDAVWrite_MKCOL_Exists(t *testing.T) {
conf := newWebDAVTestConfig(t)
if err := conf.CreateDirectories(); err != nil {
t.Fatalf("failed to create test directories: %v", err)
}
r := setupWebDAVRouter(conf)
// First MKCOL creates the collection.
w := httptest.NewRecorder()
req := httptest.NewRequest(header.MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/exists", nil)
authBearer(req)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
// Capture the console-only system log while repeating the MKCOL against the
// existing collection. Errors from x/net/webdav are routed through
// event.SystemLog (never the browser log.* stream), so we hook it here.
hook := &logCapture{}
event.SystemLog.ReplaceHooks(logrus.LevelHooks{})
event.SystemLog.AddHook(hook)
defer event.SystemLog.ReplaceHooks(logrus.LevelHooks{})
w = httptest.NewRecorder()
req = httptest.NewRequest(header.MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/exists", nil)
authBearer(req)
r.ServeHTTP(w, req)
// Probing an existing collection is a client-side no-op; x/net/webdav returns 405.
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
// The benign probe must not be logged as an error, and must not leak the server path.
var found bool
for _, e := range hook.entries {
if e.Level == logrus.DebugLevel && assert.Contains(t, e.Message, "already exists") {
found = true
assert.NotContains(t, e.Message, conf.OriginalsPath())
}
assert.NotEqual(t, logrus.ErrorLevel, e.Level, "MKCOL on existing collection must not log an error")
}
assert.True(t, found, "expected a debug entry for the existing collection")
}
func TestWebDAVWrite_PUT_OriginalsLimit(t *testing.T) {
conf := newWebDAVTestConfig(t)
conf.Options().OriginalsLimit = 1 // cap uploaded files at 1 MB