diff --git a/backend/internal/server/export.go b/backend/internal/server/export.go index 117959a..8e4dbd8 100644 --- a/backend/internal/server/export.go +++ b/backend/internal/server/export.go @@ -176,11 +176,15 @@ func subtleNeq(got, want string) bool { return !hmac.Equal([]byte(got), []byte(want)) } -// serveAttachment writes bytes as a named file download. +// serveAttachment writes bytes as a named file download. Content-Length is set +// explicitly: a body over net/http's write buffer otherwise goes out chunked, and +// Android's system DownloadManager (which VKWebAppDownloadFile delegates to) hangs +// forever on a download of unknown length. func serveAttachment(c *gin.Context, filename, contentType string, data []byte) { c.Header("X-Content-Type-Options", "nosniff") c.Header("Content-Disposition", `attachment; filename="`+sanitizeFilename(filename)+`"`) c.Header("Cache-Control", "private, max-age=0") + c.Header("Content-Length", strconv.Itoa(len(data))) c.Data(http.StatusOK, contentType, data) } diff --git a/gateway/internal/connectsrv/server.go b/gateway/internal/connectsrv/server.go index 590d46b..c703dfd 100644 --- a/gateway/internal/connectsrv/server.go +++ b/gateway/internal/connectsrv/server.go @@ -14,6 +14,7 @@ import ( "io" "net" "net/http" + "strconv" "strings" "time" @@ -465,6 +466,9 @@ func (s *Server) exportDownloadHandler() http.Handler { } w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Cache-Control", "private, max-age=0") + // Explicit length, or the body goes out chunked and Android's system + // DownloadManager (the VKWebAppDownloadFile executor) hangs on it. + w.Header().Set("Content-Length", strconv.Itoa(len(data))) _, _ = w.Write(data) }) }