blob: 48573d8a7715da4c66ce9150ae3238968d078361 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package sse
// TODO(raggi): add support for some kind of read limits per event and so on
import (
"bufio"
"fmt"
"net/http"
"strconv"
"strings"
)
// ProtocolError provides details of any protocol error that has occurred, the
// original Response object and an Error that contains a description of the
// problem.
type ProtocolError struct {
Response *http.Response
Err error
}
func (pe *ProtocolError) Error() string {
return fmt.Sprintf("sse: protocol error: %s", pe.Err.Error())
}
// Client provides a handle to methods that are bound to a single SSE stream.
type Client struct {
r *http.Response
b *bufio.Reader
}
// New constructs a new SSE client against the http.Response. It returns an
// error if the response is not a valid SSE response, for example carrying the
// incorrect content type, or not having a response body stream. In cases where
// the error is an SSE protocol issue, ProtocolError is returned.
func New(r *http.Response) (*Client, error) {
if r.ContentLength != -1 {
return nil, &ProtocolError{r, fmt.Errorf("fixed content-length: %d", r.ContentLength)}
}
if !strings.Contains(r.Header.Get("Content-Type"), "text/event-stream") {
return nil, &ProtocolError{r, fmt.Errorf("invalid content-type: %s", r.Header.Get("Content-Type"))}
}
return &Client{r, bufio.NewReader(r.Body)}, nil
}
// ReadEvent parses the next event out of the response body, blocking if none is
// yet available. It returns either an Event or an error. error is either a
// ProtocolError or one of various IO errors, such as io.EOF.
func (c *Client) ReadEvent() (*Event, error) {
var e *Event
for {
line, err := c.b.ReadString('\n')
if err != nil {
return nil, err
}
if len(line) == 0 || line[0] == '\n' {
if e != nil {
return e, nil
}
continue
}
if line[0] == ':' {
continue
}
var value string
parts := strings.SplitN(strings.TrimRight(line, "\r\n"), ":", 2)
if len(parts) == 2 {
value = strings.TrimPrefix(parts[1], " ")
}
// TODO(raggi): if fields are repeated (other than data) decide whether to
// catch this and error on it. Right now the last field wins.
switch parts[0] {
case "event":
if e == nil {
e = &Event{}
}
e.Event = value
case "data":
if e == nil {
e = &Event{}
}
if len(e.Data) > 0 {
e.Data = append(e.Data, '\n')
}
e.Data = append(e.Data, value...)
case "id":
if e == nil {
e = &Event{}
}
e.ID = value
case "retry":
if e == nil {
e = &Event{}
}
i, err := strconv.Atoi(value)
if err != nil {
return e, &ProtocolError{c.r, fmt.Errorf("invalid retry value: %q", value)}
}
e.Retry = &i
}
}
}