Closes #56 Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #63.
This commit is contained in:
+48
-1
@@ -84,7 +84,7 @@ func (a *App) handleUploadPost(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, a.UploadLimits.MaxBytes+uploadOverhead)
|
||||
r.Body = http.MaxBytesReader(w, a.idleBoundedBody(w, r.Body), a.UploadLimits.MaxBytes+uploadOverhead)
|
||||
mr, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
a.renderUploadsPage(w, r, userID, http.StatusBadRequest, "Choose an audio file to upload.")
|
||||
@@ -158,6 +158,53 @@ func (a *App) handleUploadPost(w http.ResponseWriter, r *http.Request) {
|
||||
a.renderUploadsPage(w, r, userID, http.StatusBadRequest, "Choose an audio file to upload.")
|
||||
}
|
||||
|
||||
// defaultUploadIdleTimeout is how long an upload body may deliver nothing before its
|
||||
// connection read is cut off (#56). A stall this long is not a slow link but a dead or
|
||||
// hostile one; a legitimate upload, however slow, keeps delivering bytes.
|
||||
const defaultUploadIdleTimeout = 60 * time.Second
|
||||
|
||||
// uploadIdleTimeout is the idle bound this App applies: UploadIdleTimeout when set, else
|
||||
// defaultUploadIdleTimeout. cmd/tapir leaves it unset, so production runs the default.
|
||||
func (a *App) uploadIdleTimeout() time.Duration {
|
||||
if a.UploadIdleTimeout <= 0 {
|
||||
return defaultUploadIdleTimeout
|
||||
}
|
||||
return a.UploadIdleTimeout
|
||||
}
|
||||
|
||||
// idleBoundedBody bounds the SILENCE in an upload body, not its total duration (#56): every
|
||||
// read first pushes the connection's read deadline idle into the future, so a body that
|
||||
// keeps moving is never cut, and one that stops fails its read with a timeout. That read
|
||||
// error reaches Accept as a client read error, so Accept returns and its receive slot is
|
||||
// released. A whole-request ReadTimeout would instead cut a large upload on a slow link.
|
||||
//
|
||||
// If the writer cannot set a read deadline (a wrapper without Unwrap, or a test recorder)
|
||||
// the body is returned unbounded and that is logged: a control that silently does not
|
||||
// apply is worse than one that says so.
|
||||
func (a *App) idleBoundedBody(w http.ResponseWriter, body io.ReadCloser) io.ReadCloser {
|
||||
idle := a.uploadIdleTimeout()
|
||||
rc := http.NewResponseController(w)
|
||||
if err := rc.SetReadDeadline(time.Now().Add(idle)); err != nil {
|
||||
a.logger().Warn("upload idle bound unavailable", "err", err)
|
||||
return body
|
||||
}
|
||||
return &idleDeadlineBody{ReadCloser: body, rc: rc, idle: idle}
|
||||
}
|
||||
|
||||
type idleDeadlineBody struct {
|
||||
io.ReadCloser
|
||||
rc *http.ResponseController
|
||||
idle time.Duration
|
||||
}
|
||||
|
||||
func (b *idleDeadlineBody) Read(p []byte) (int, error) {
|
||||
_ = b.rc.SetReadDeadline(time.Now().Add(b.idle))
|
||||
// Never cleared here: net/http clears the read deadline itself before its background
|
||||
// read once the handler has the body, and an armed deadline after an error also bounds
|
||||
// net/http's drain of an unread remainder before it replies.
|
||||
return b.ReadCloser.Read(p)
|
||||
}
|
||||
|
||||
// abandoned reports, logs at INFO and answers 499, because the client has already gone: its request
|
||||
// context is cancelled, so there is nobody to answer and rendering a page would run its
|
||||
// database read on a dead context and log a false ERROR. A hang-up mid-upload is routine.
|
||||
|
||||
Reference in New Issue
Block a user