Repository: AlistGo/alist (language: go)

Issue:
某些存储驱动开启“本地代理”时，如果下载文件时网络中断，那么下载的文件会损坏
### Please make sure of the following things

- [x] I have read the [documentation](https://alist.nn.ci).
我已经阅读了[文档](https://alist.nn.ci)。

- [x] I'm sure there are no duplicate issues or discussions.
我确定没有重复的issue或讨论。

- [x] I'm sure it's due to `AList` and not something else(such as [Network](https://alist.nn.ci/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`).
我确定是`AList`的问题，而不是其他原因（例如[网络](https://alist.nn.ci/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host)，`依赖`或`操作`）。

- [x] I'm sure this issue is not fixed in the latest version.
我确定这个问题在最新版本中没有被修复。


### AList Version / AList 版本

v3.44.0

### Driver used / 使用的存储驱动

123云盘

### Describe the bug / 问题描述

对于某些存储驱动，比如123云盘，当“WebDAV 策略”设置成“本地代理”，并且“下载代理URL”为空时时，如果下载文件时网络中断，那么下载的文件会损坏，文件损坏的部分被替换成了字符串`Internal Server Error`。

经过我粗略地查看alist的源码，发现该字符串是在源码文件 [server/webdav/webdav.go](https://github.com/AlistGo/alist/blob/0cde4e73d614a4bf0b28872391a13d0f8a6d166a/server/webdav/webdav.go#L89) 写入的，过程如下：

1. 实现本地代理的函数 `common.Proxy()` 因网络中断而产生错误。
2. 在函数`handleGetHeadPost()`里，`common.Proxy()`产生的错误被统一当成了`http.StatusInternalServerError`，该常量的值是`500`。
3. 函数`handleGetHeadPost()`携带错误码`500`返回到上层函数`ServeHTTP()`
4. 在函数`ServeHTTP()`里，数值`500`会被函数`StatusText()`转换成文本字符串`Internal Server Error`
5. 在函数`ServeHTTP()`里，该字符串通过函数`w.Write()`写入到下载的文件流中。


具体的代码如下：
```go
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if h.LockSystem == nil {
	} else {
		switch r.Method {
		case "GET", "HEAD", "POST":
			status, err = h.handleGetHeadPost(w, r)
		}
	}

	if status != 0 {
		if status != http.StatusNoContent {
			w.Write([]byte(StatusText(status)))
		}
	}
}

func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {
	if storage.GetStorage().WebdavNative() || (storage.GetStorage().WebdavProxy() && downProxyUrl == "") {
		err = common.Proxy(w, r, link, fi)
		if err != nil {
			log.Errorf("webdav proxy error: %+v", err)
			return http.StatusInternalServerError, err
		}
	}
}

func StatusText(code int) string {
	return http.StatusText(code)
}
```

我目前知道， `123云盘` 存储驱动会受该bug的影响， `UC网盘` 不受该bug影响（但也会出现文件损坏的情况），其他的存储驱动没有试验。考虑到`server/webdav/webdav.go`好像是个通用模块，所以其他驱动可能也有问题。




### Reproduction / 复现链接

下面说一下怎么复现一个损坏的文件：

1. 在alist的123云盘驱动里，把“WebDAV 策略”设置成“本地代理”，并且“下载代理URL”为空。
2. 安装`rclone`并配置类型为`webdav`远程存储，指向alist。
3. 执行`"rclone copyto alist:123云盘/aa.bin" /test.bin --multi-thread-streams=0 -P -vv` 来下载一个文件
4. 执行`ss -tnp state established | grep alist`来看看alist建立的到网盘的TCP连接，该命令输出形如：   
 ```
    0    0    192.168.1.1:48568    2.2.2.2:443   users:(("alist",pid=113750,fd=10))                     
    0    0    192.168.1.1:44790    3.3.3.3:443   users:(("alist",pid=113750,fd=12))                     
    5760    0    192.168.1.1:44784    4.4.4.4:443   users:(("alist",pid=113750,fd=11)) 
 ```

注意其中第一第二列不为0的行，说明它们有流量，正是alist到网盘的连接

5. 执行`ss -K dst 4.4.4.4 dport = 443`，该命令根据远端IP和端口来关闭连接
6. 当alist到网盘的连接被关闭时，rclone会提示文件错误的偏移量，rclone的输出形如：
```
2025/04/02 18:52:11 DEBUG : aa.bin: Reopening on read failure after offset 46139230 bytes: retry 2/10: unexpected EOF
```
7. 我们需要查看偏移量的前N个字节才能看到完整的错误提示字符串，所以我把查看的范围设定到偏移量的前100个字节，执行以下命令：`hexdump -C -n 512 -s 46139130 /test.bin*`，这样在输出中应该能看到字符串`Internal Server Error`。

### Config / 配置

无。

### Logs / 日志

_No response_


Discussion / hints:
感谢您详细描述了这个问题。根据您的描述，问题似乎出现在网络中断时，错误信息被写入到下载的文件中，导致文件损坏。这个问题可能与代码中错误处理的方式有关。

为了更好地解决这个问题，可以考虑以下几种可能的改进方案：

1. **错误处理改进**：在网络中断时，避免将错误信息直接写入到文件流中。可以考虑在`handleGetHeadPost`函数中捕获错误，并在返回错误码之前，确保不会将错误信息写入到文件流中。

2. **重试机制**：在网络中断时，增加重试机制，尝试重新建立连接并继续下载，而不是直接返回错误。

3. **日志记录**：将错误信息记录到日志中，而不是写入到文件流中，以便后续分析和调试。

如果您有兴趣，我可以帮助您在代码中进行具体的修改建议。请让我知道您的想法。

<details>
<summary>🪧 Tips</summary>
For further assistance, please describe your question in the comments and @petercat-assistant to start a conversation with me.
</details>

Fix patch:
diff --git a/internal/net/serve.go b/internal/net/serve.go
index 63e1cb45b87..bdeac0ac5f8 100644
--- a/internal/net/serve.go
+++ b/internal/net/serve.go
@@ -52,19 +52,19 @@ import (
 //
 // If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
 // ServeHTTP uses it to handle requests using If-Match, If-None-Match, or If-Range.
-func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time.Time, size int64, RangeReadCloser model.RangeReadCloserIF) {
+func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time.Time, size int64, RangeReadCloser model.RangeReadCloserIF) error {
 	defer RangeReadCloser.Close()
 	setLastModified(w, modTime)
 	done, rangeReq := checkPreconditions(w, r, modTime)
 	if done {
-		return
+		return nil
 	}
 
 	if size < 0 {
 		// since too many functions need file size to work,
 		// will not implement the support of unknown file size here
 		http.Error(w, "negative content size not supported", http.StatusInternalServerError)
-		return
+		return nil
 	}
 
 	code := http.StatusOK
@@ -103,7 +103,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
 		fallthrough
 	default:
 		http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
-		return
+		return nil
 	}
 
 	if sumRangesSize(ranges) > size {
@@ -124,7 +124,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
 				code = http.StatusTooManyRequests
 			}
 			http.Error(w, err.Error(), code)
-			return
+			return nil
 		}
 		sendContent = reader
 	case len(ranges) == 1:
@@ -147,7 +147,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
 				code = http.StatusTooManyRequests
 			}
 			http.Error(w, err.Error(), code)
-			return
+			return nil
 		}
 		sendSize = ra.Length
 		code = http.StatusPartialContent
@@ -205,9 +205,11 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
 			if err == ErrExceedMaxConcurrency {
 				code = http.StatusTooManyRequests
 			}
-			http.Error(w, err.Error(), code)
+			w.WriteHeader(code)
+			return err
 		}
 	}
+	return nil
 }
 func ProcessHeader(origin, override http.Header) http.Header {
 	result := http.Header{}
diff --git a/server/common/proxy.go b/server/common/proxy.go
index f9e1e4bb0e8..ca7f6325d7d 100644
--- a/server/common/proxy.go
+++ b/server/common/proxy.go
@@ -39,11 +39,10 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
 		return nil
 	} else if link.RangeReadCloser != nil {
 		attachHeader(w, file)
-		net.ServeHTTP(w, r, file.GetName(), file.ModTime(), file.GetSize(), &stream.RateLimitRangeReadCloser{
+		return net.ServeHTTP(w, r, file.GetName(), file.ModTime(), file.GetSize(), &stream.RateLimitRangeReadCloser{
 			RangeReadCloserIF: link.RangeReadCloser,
 			Limiter:           stream.ServerDownloadLimit,
 		})
-		return nil
 	} else if link.Concurrency != 0 || link.PartSize != 0 {
 		attachHeader(w, file)
 		size := file.GetSize()
@@ -66,11 +65,10 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
 			rc, err := down.Download(ctx, req)
 			return rc, err
 		}
-		net.ServeHTTP(w, r, file.GetName(), file.ModTime(), file.GetSize(), &stream.RateLimitRangeReadCloser{
+		return net.ServeHTTP(w, r, file.GetName(), file.ModTime(), file.GetSize(), &stream.RateLimitRangeReadCloser{
 			RangeReadCloserIF: &model.RangeReadCloser{RangeReader: rangeReader},
 			Limiter:           stream.ServerDownloadLimit,
 		})
-		return nil
 	} else {
 		//transparent proxy
 		header := net.ProcessHeader(r.Header, link.Header)
@@ -90,10 +88,7 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
 			Limiter: stream.ServerDownloadLimit,
 			Ctx:     r.Context(),
 		})
-		if err != nil {
-			return err
-		}
-		return nil
+		return err
 	}
 }
 func attachHeader(w http.ResponseWriter, file model.Obj) {
@@ -133,3 +128,29 @@ func ProxyRange(link *model.Link, size int64) {
 		link.RangeReadCloser = nil
 	}
 }
+
+type InterceptResponseWriter struct {
+	http.ResponseWriter
+	io.Writer
+}
+
+func (iw *InterceptResponseWriter) Write(p []byte) (int, error) {
+	return iw.Writer.Write(p)
+}
+
+type WrittenResponseWriter struct {
+	http.ResponseWriter
+	written bool
+}
+
+func (ww *WrittenResponseWriter) Write(p []byte) (int, error) {
+	n, err := ww.ResponseWriter.Write(p)
+	if !ww.written && n > 0 {
+		ww.written = true
+	}
+	return n, err
+}
+
+func (ww *WrittenResponseWriter) IsWritten() bool {
+	return ww.written
+}
diff --git a/server/handles/down.go b/server/handles/down.go
index 1153881fdda..2c5c2fafc51 100644
--- a/server/handles/down.go
+++ b/server/handles/down.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"net/http"
 	stdpath "path"
 	"strconv"
 	"strings"
@@ -129,15 +128,16 @@ func localProxy(c *gin.Context, link *model.Link, file model.Obj, proxyRange boo
 	if proxyRange {
 		common.ProxyRange(link, file.GetSize())
 	}
+	Writer := &common.WrittenResponseWriter{ResponseWriter: c.Writer}
 
 	//优先处理md文件
 	if utils.Ext(file.GetName()) == "md" && setting.GetBool(conf.FilterReadMeScripts) {
-		w := c.Writer
 		buf := bytes.NewBuffer(make([]byte, 0, file.GetSize()))
-		err = common.Proxy(&proxyResponseWriter{ResponseWriter: w, Writer: buf}, c.Request, link, file)
+		w := &common.InterceptResponseWriter{ResponseWriter: Writer, Writer: buf}
+		err = common.Proxy(w, c.Request, link, file)
 		if err == nil && buf.Len() > 0 {
-			if w.Status() < 200 || w.Status() > 300 {
-				w.Write(buf.Bytes())
+			if c.Writer.Status() < 200 || c.Writer.Status() > 300 {
+				c.Writer.Write(buf.Bytes())
 				return
 			}
 
@@ -148,19 +148,23 @@ func localProxy(c *gin.Context, link *model.Link, file model.Obj, proxyRange boo
 				buf.Reset()
 				err = bluemonday.UGCPolicy().SanitizeReaderToWriter(&html, buf)
 				if err == nil {
-					w.Header().Set("Content-Length", strconv.FormatInt(int64(buf.Len()), 10))
-					w.Header().Set("Content-Type", "text/html; charset=utf-8")
-					_, err = utils.CopyWithBuffer(c.Writer, buf)
+					Writer.Header().Set("Content-Length", strconv.FormatInt(int64(buf.Len()), 10))
+					Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
+					_, err = utils.CopyWithBuffer(Writer, buf)
 				}
 			}
 		}
 	} else {
-		err = common.Proxy(c.Writer, c.Request, link, file)
+		err = common.Proxy(Writer, c.Request, link, file)
 	}
-	if err != nil {
-		common.ErrorResp(c, err, 500, true)
+	if err == nil {
 		return
 	}
+	if Writer.IsWritten() {
+		log.Errorf("%s %s local proxy error: %+v", c.Request.Method, c.Request.URL.Path, err)
+	} else {
+		common.ErrorResp(c, err, 500, true)
+	}
 }
 
 // TODO need optimize
@@ -182,12 +186,3 @@ func canProxy(storage driver.Driver, filename string) bool {
 	}
 	return false
 }
-
-type proxyResponseWriter struct {
-	http.ResponseWriter
-	io.Writer
-}
-
-func (pw *proxyResponseWriter) Write(p []byte) (int, error) {
-	return pw.Writer.Write(p)
-}
diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go
index 1b7ec6ff72b..f22e15aadb9 100644
--- a/server/webdav/webdav.go
+++ b/server/webdav/webdav.go
@@ -24,7 +24,6 @@ import (
 	"github.com/alist-org/alist/v3/internal/sign"
 	"github.com/alist-org/alist/v3/pkg/utils"
 	"github.com/alist-org/alist/v3/server/common"
-	log "github.com/sirupsen/logrus"
 )
 
 type Handler struct {
@@ -59,7 +58,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			status, err = h.handleOptions(brw, r)
 		case "GET", "HEAD", "POST":
 			useBufferedWriter = false
-			status, err = h.handleGetHeadPost(w, r)
+			Writer := &common.WrittenResponseWriter{ResponseWriter: w}
+			status, err = h.handleGetHeadPost(Writer, r)
+			if status != 0 && Writer.IsWritten() {
+				status = 0
+			}
 		case "DELETE":
 			status, err = h.handleDelete(brw, r)
 		case "PUT":
@@ -247,8 +250,7 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
 		}
 		err = common.Proxy(w, r, link, fi)
 		if err != nil {
-			log.Errorf("webdav proxy error: %+v", err)
-			return http.StatusInternalServerError, err
+			return http.StatusInternalServerError, fmt.Errorf("webdav proxy error: %+v", err)
 		}
 	} else if storage.GetStorage().WebdavProxy() && downProxyUrl != "" {
 		u := fmt.Sprintf("%s%s?sign=%s",


Test patch: