From 5662318e1c80a8905b6f793654fdcde3f809b35a Mon Sep 17 00:00:00 2001 From: Thanatat Tamtan Date: Sun, 28 Jun 2026 23:51:27 +0700 Subject: [PATCH] dropbox: detach empty-upload cleanup from the request context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a chunked (unknown-length) request carries an empty body, the handler finalizes a 0-byte object and then deletes it before returning 'body empty'. That delete used r.Context(), which is canceled the moment the client disconnects — and a client that sends an empty body and drops the connection is exactly the case that triggers this path. A canceled-context delete fails, stranding a rowless 0-byte object that the DB-driven GC can never reclaim. Use the existing deleteObject helper, which detaches via context.WithoutCancel + a 10s timeout — the same pattern already used by the mid-stream io.Copy error path and the signed-upload rejection paths. No behavior change for the success path. go build / vet clean; existing TestUpload_EmptyChunkedBody (and the rest of the upload suite) still green. --- handler.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/handler.go b/handler.go index 596e6ca..d15410e 100644 --- a/handler.go +++ b/handler.go @@ -137,11 +137,13 @@ func (a *App) uploadHandler(w http.ResponseWriter, r *http.Request) { // that only becomes apparent after io.Copy reports n == 0. Storing a // 0-byte object serves no one and would linger in the bucket, so delete // the object we just finalized and bail before writing any DB row (GC is - // DB-driven and would never reclaim a bucket object with no row). + // DB-driven and would never reclaim a bucket object with no row). Use + // deleteObject, which detaches from r.Context(): a client that disconnects + // right after the empty body cancels r.Context(), and reusing it here would + // fail the cleanup and strand the rowless object (same reason as the + // mid-stream error path above). if n == 0 { - if err := a.Bucket.Delete(r.Context(), fn); err != nil && gcerrors.Code(err) != gcerrors.NotFound { - slog.Error("delete empty upload", "fn", fn, "error", err) - } + a.deleteObject(r, fn) jsonFail(w, "body empty", http.StatusOK) return }