From c16e9e292fc40f55d7f8769854caec3479c3eb92 Mon Sep 17 00:00:00 2001 From: Andrej Mickov Date: Sun, 26 Jul 2026 12:28:13 +0200 Subject: [PATCH] Avoid truncating large uploads with body timeouts http.Server.ReadTimeout and WriteTimeout cover the full request/ response lifetime including streaming bodies, so finite values truncate large object PUTs and GETs mid-flight. Disable both and rely on ReadHeaderTimeout (header phase) and IdleTimeout (between keepalive requests), consistent with how S3-style streaming servers behave. serverMaxConnections still bounds concurrent load. Also log io.Copy errors in handleGetObject so a client disconnect or mid-stream chunk failure is no longer invisible. --- api/api.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/api/api.go b/api/api.go index 6a3f6aa..ae59068 100644 --- a/api/api.go +++ b/api/api.go @@ -43,11 +43,17 @@ const ( maxObjectKeyBytes = 1024 maxAWSChunkedLineBytes = 8 << 10 serverReadHeaderTimeout = 5 * time.Second - serverReadTimeout = 60 * time.Second - serverWriteTimeout = 120 * time.Second - serverIdleTimeout = 120 * time.Second - serverMaxHeaderBytes = 1 << 20 - serverMaxConnections = 1024 + // serverReadTimeout and serverWriteTimeout are disabled on purpose: both + // cover the full request/response lifetime including streaming bodies, so a + // finite value truncates large uploads and downloads mid-flight. Read and + // idle liveness are still enforced by serverReadHeaderTimeout (header + // phase) and serverIdleTimeout (between keepalive requests), and concurrent + // load is bounded by serverMaxConnections. + serverReadTimeout = 0 + serverWriteTimeout = 0 + serverIdleTimeout = 120 * time.Second + serverMaxHeaderBytes = 1 << 20 + serverMaxConnections = 1024 ) func NewHandler(svc *service.ObjectService, logger *slog.Logger, logConfig logging.Config, authSvc *auth.Service, adminAPI bool) *Handler { @@ -271,8 +277,14 @@ func (h *Handler) handleGetObject(w http.ResponseWriter, r *http.Request) { w.Header().Set("Last-Modified", time.Unix(manifest.CreatedAt, 0).UTC().Format(http.TimeFormat)) w.Header().Set("Accept-Ranges", "bytes") w.WriteHeader(http.StatusOK) - _, err = io.Copy(w, stream) - + if _, err = io.Copy(w, stream); err != nil && !errors.Is(err, context.Canceled) { + h.logger.Warn("get_object_stream_failed", + "bucket", bucket, + "key", key, + "etag", manifest.ETag, + "error", err, + ) + } } func (h *Handler) handlePostObject(w http.ResponseWriter, r *http.Request) {