📖 3 min read (~ 500 words).

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 chosen

Full 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

$ go run ./stream-server/cmd/countdown-server --port=8000
$ curl -N http://127.0.0.1:8000/elapse/5
{"remains":5}
{"remains":4}
{"remains":3}
{"remains":2}
{"remains":1}
{"remains":0}

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).