echo
echo copied to clipboard
Stream() does not listen to cancelled context
Issue Description
I am/was planning to use ctx.Stream(), as I need to send io.Reader as responses to our endpoints (they are processed files, which are fully processed as streams to prevent huge memory requirements).
It all works nicely, until I noticed that it is not possible to (gracefully) shutdown the webserver when responses 'hang' (for whatever reason).
When checking the Steam() code it makes sense:
func (c *context) Stream(code int, contentType string, r io.Reader) (err error) {
c.writeContentType(contentType)
c.response.WriteHeader(code)
_, err = io.Copy(c.response, r)
return
}
as io.Copy is not context aware.
Checklist
- [ ] Dependencies installed
- [ ] No typos
- [x] Searched existing issues ~and docs~
Expected behaviour
I would expect in a server/service environment that calls to send responses can be cancelled/aborted, in case of
- server needs to shutdown
- client connection is broken (no need to continue processing/streaming)
- timeouts
- and probably more
I feel that some thing like this (pseudocode-ish) is sometinh one would expect for Steam():
for {
select {
case "can read from stream":
// read from stream
case io.EOF:
// Done, stream finished
return nil
case ctx.Done():
return ctx.Err()
}
}
Actual behaviour
It is not possible to cancel Steam() using the context, most probably other 'response functions' as well? But the Stream() function particularly is expected to handle context cancallations, as it typically might take a while handle (potentially large, 'on the fly' processing) streams.
Steps to reproduce
Working code to debug
type NeverEndingReader struct { }
func (t Thing) Read(dst []byte) (n int, err error) {
dst[0] = 'a'
return 1, nil
}
func Handle(ctx echo.Context) error {
return ctx.Stream(http.StatusOK, "application/text", &NeverEndingReader);
}
Didn't run the code, but Handle() will obviously never finish as the reader will always be able to return more 'data'...
So timeouts, or (gracefully) shutting down the server will also not be able to stop this.
Version/commit
v4.6.3
Well it depends - except timeout middleware Echo does not try to handle or guess when or how request context ends (c.Request().Context()). Currently this is left up to developer to handle these cases or http.Server just to terminate connection. You may want to handle these cases cases with pre/post steps etc - use context-aware reader/writer. Maybe something like that https://pace.dev/blog/2020/02/03/context-aware-ioreader-for-golang-by-mat-ryer.html
Having a context-aware io.Reader could indeed be good alternative (didn't think of it :blush:).
I was just assuming that such 'trivialities' would be handled by the server/router. As these (timeouts, connection drops, etc.) are very common scenarios for web-based software. Then again, keeping a framework as light/performant as possible also makes sense, so letting devs implement this themselves when needed is (IMO) also a fine solution.
Maybe adding a context-aware example (with dev implementation :wink: ) would help future devs to figure this out?
Anyway, I am fine with closing this discussion/ticket. Thanks a lot for all the work on this fine framework :+1:
Do not close this ticket yet. We might come up with something.
Go reader/writer are quite cumbersome (low level) to use if it comes to timeouts/cancellation - I have had to implement "cancellable" readers for reading USB/serial devices. There are SetReadDeadline() and SetDeadline().
I'm adding couple of links to libraries that try to address this issue:
- https://github.com/dolmen-go/contextio Context-aware I/O streams for Go
- https://github.com/jbenet/go-context/io Context-aware reader and writer
- https://github.com/northbright/ctx/ctxcopy Context-aware io.Copy
- https://gitlab.com/streamy/concon Context-aware net.Conn