Streaming server
Swagger 2.0 has no first-class notion of a streaming response, but you can still
generate a server that streams. The stream-server example is a countdown API:
GET /elapse/{length} emits one newline-delimited JSON object per second until it
reaches zero.
Tip
Source: stream-server/.
Generated with swagger generate server --spec ./swagger.yml.
Declaring a streaming response
The trick is the response schema: type: string, format: binary. That’s the
closest Swagger 2.0 gets to “this endpoint streams bytes”, and it makes the
generator produce a response the handler writes to directly rather than a typed
payload it serializes for you:
responses:
200:
description: Secondly update on remaining time
# This is the best representation there is in Swagger 2.0 to
# say that this endpoint has a streaming response.
# In this implementation, it will be newline delimited JSON
# bodies, of the `Mark` type defined below
schema:
type: string
format: binary
403:
description: Contrived - thrown when length of 11 is chosenFull source: stream-server/swagger.yml
Writing the stream from the handler
Instead of returning a generated responder, the handler returns a
middleware.ResponderFunc — a closure with raw access to the
http.ResponseWriter. It grabs the http.Flusher and writes through a small
wrapper so each write is pushed to the client immediately:
api.ElapseHandler = operations.ElapseHandlerFunc(func(params operations.ElapseParams) middleware.Responder {
if params.Length == 11 {
return operations.NewElapseForbidden()
}
return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
f, _ := rw.(http.Flusher)
rw.WriteHeader(http.StatusOK)
_ = myCounter.Down(params.Length, &flushWriter{f: f, w: rw})
})
})Full source: stream-server/restapi/configure_countdown.go
The flushWriter is what turns a normal write into a streamed chunk — it flushes
after every write, so the client sees each line as it’s produced rather than at
the end:
// Via https://play.golang.org/p/PpbPyXbtEs
type flushWriter struct {
f http.Flusher
w io.Writer
}
// Via https://play.golang.org/p/PpbPyXbtEs
func (fw *flushWriter) Write(p []byte) (n int, err error) {
n, err = fw.w.Write(p)
if fw.f != nil {
fw.f.Flush()
}
return
}Full source: stream-server/restapi/configure_countdown.go
Producing the chunks
The business logic just encodes one Mark per iteration into the writer, with a
one-second pause between them. Because the writer flushes on every Encode, each
{"remains":N} line reaches the client in real time:
// Down is the concrete implementation that spits out the JSON bodies.
func (mc *MyCounter) Down(maximum int64, w io.Writer) error {
if maximum == 11 {
return errors.New("we don't *do* elevensies")
}
e := json.NewEncoder(w)
for ix := int64(0); ix <= maximum; ix++ {
r := maximum - ix
fmt.Printf("Iteration %d\n", r)
if err := e.Encode(models.Mark{Remains: &r}); err != nil {
return err
}
if ix != maximum {
time.Sleep(1 * time.Second)
}
}
return nil
}Full source: stream-server/biz/count.go
Trying it
The response uses Transfer-Encoding: chunked; each line arrives a second apart.
A length of 11 returns 403 (a contrived error to show non-streaming responses
still work normally).
Related
- Streaming client — consuming this stream from a generated client.
- Hand-wiring a streaming server without codegen? See the go-openapi/runtime examples.